diff --git a/.env.example b/.env.example
index 12542ad..dfe77d0 100644
--- a/.env.example
+++ b/.env.example
@@ -23,6 +23,7 @@ DB_PASSWORD=postgres
FPH_ENABLED=true
CSP_ENABLED=true
+CSP_NONCE_ENABLED=true
SESSION_DRIVER=database
SESSION_LIFETIME=120
diff --git a/app/Security/Presets/MyCspPreset.php b/app/Security/Presets/MyCspPreset.php
index e3c48f6..d5f1830 100644
--- a/app/Security/Presets/MyCspPreset.php
+++ b/app/Security/Presets/MyCspPreset.php
@@ -35,6 +35,10 @@ public function configure(Policy $policy): void
$policy->add(Directive::SCRIPT_ELEM, $scriptSources);
+ $policy->addNonce(Directive::SCRIPT);
+
+ $policy->addNonce(Directive::SCRIPT_ELEM);
+
$styleSources = array_filter([
Keyword::SELF,
$cdnHost ?: null,
diff --git a/app/Security/Replacers/CspHeaderReplacer.php b/app/Security/Replacers/CspHeaderReplacer.php
new file mode 100644
index 0000000..0c9e215
--- /dev/null
+++ b/app/Security/Replacers/CspHeaderReplacer.php
@@ -0,0 +1,31 @@
+ */
+ protected array $headers = [
+ 'Content-Security-Policy',
+ 'Content-Security-Policy-Report-Only',
+ ];
+
+ public function prepareResponseToCache(Response $response): void
+ {
+ foreach ($this->headers as $header) {
+ $response->headers->remove($header);
+ }
+ }
+
+ public function replaceInCachedResponse(Response $response): void
+ {
+ foreach ($this->headers as $header) {
+ $response->headers->remove($header);
+ }
+ }
+}
diff --git a/app/Seo/SchemaGraph.php b/app/Seo/SchemaGraph.php
index 8533586..1014d40 100644
--- a/app/Seo/SchemaGraph.php
+++ b/app/Seo/SchemaGraph.php
@@ -229,8 +229,8 @@ private function breadcrumbList(string $url): ?array
}
/**
- * Title of the page being rendered, stripped of the " – codebar Solutions AG"
- * suffix that belongs in
but not in a breadcrumb.
+ * Title of the page being rendered, reduced to the part before the first
+ * separator: a breadcrumb carries the label, not the full title.
*/
private function currentPageName(): ?string
{
diff --git a/config/csp.php b/config/csp.php
index 884e388..1e20dd4 100644
--- a/config/csp.php
+++ b/config/csp.php
@@ -63,5 +63,5 @@
* and cannot add inline nonces.
* Note that this will make your CSP policy less secure.
*/
- 'nonce_enabled' => env('CSP_NONCE_ENABLED', false),
+ 'nonce_enabled' => env('CSP_NONCE_ENABLED', true),
];
diff --git a/config/health.php b/config/health.php
index 13f30e8..3b23c68 100644
--- a/config/health.php
+++ b/config/health.php
@@ -26,7 +26,7 @@
/*
* Notifications will only get sent if this option is set to `true`.
*/
- 'enabled' => true,
+ 'enabled' => (bool) env('HEALTH_NOTIFICATION_EMAIL'),
'notifications' => [
CheckFailedNotification::class => ['mail'],
diff --git a/config/responsecache.php b/config/responsecache.php
index 91dc412..f308935 100644
--- a/config/responsecache.php
+++ b/config/responsecache.php
@@ -2,6 +2,7 @@
declare(strict_types=1);
+use App\Security\Replacers\CspHeaderReplacer;
use Spatie\ResponseCache\CacheProfiles\CacheAllSuccessfulGetRequests;
use Spatie\ResponseCache\Hasher\DefaultHasher;
use Spatie\ResponseCache\Replacers\CsrfTokenReplacer;
@@ -123,5 +124,6 @@
*/
'replacers' => [
CsrfTokenReplacer::class,
+ CspHeaderReplacer::class,
],
];
diff --git a/database/files/pages/about-us.index.yaml b/database/files/pages/about-us.index.yaml
index 400110e..1c851eb 100644
--- a/database/files/pages/about-us.index.yaml
+++ b/database/files/pages/about-us.index.yaml
@@ -2,8 +2,8 @@ key: about-us.index
robots: index,follow
image: images/pages/about-us.index.svg
title:
- de_CH: 'Team – die Menschen hinter codebar Solutions AG'
- en_CH: 'Team – the People Behind codebar Solutions AG'
+ de_CH: 'Team – die Menschen hinter codebar'
+ en_CH: 'Team – the People Behind codebar'
description:
de_CH: 'Klein aus Überzeugung: kurze Wege, direkte Ansprechpersonen – und Ausbildung als Teil unserer Kultur. Lerne die Menschen hinter codebar kennen.'
en_CH: 'Small by conviction: short paths, direct contacts – and training as part of our culture. Meet the people behind codebar.'
diff --git a/database/files/pages/ai.index.yaml b/database/files/pages/ai.index.yaml
index ab6a657..7b52b58 100644
--- a/database/files/pages/ai.index.yaml
+++ b/database/files/pages/ai.index.yaml
@@ -2,8 +2,8 @@ key: ai.index
robots: index,follow
image: images/pages/ai.index.svg
title:
- de_CH: 'Lokale KI auf eigener Infrastruktur | codebar Solutions AG'
- en_CH: 'Local AI on Our Own Infrastructure | codebar Solutions AG'
+ de_CH: 'Lokale KI auf eigener Infrastruktur'
+ en_CH: 'Local AI on Our Own Infrastructure'
description:
de_CH: 'Transparenz statt Buzzwords: Wir zeigen offen, wie wir KI in der eigenen Arbeit einsetzen – mit lokalen Open-Source-Modellen und echten Nutzungszahlen.'
en_CH: 'Transparency over buzzwords: an open look at how we use AI in our own work – with local open-source models and real usage figures.'
diff --git a/database/files/pages/ai.llm.analytics.index.yaml b/database/files/pages/ai.llm.analytics.index.yaml
index 5ab7ac3..1f49779 100644
--- a/database/files/pages/ai.llm.analytics.index.yaml
+++ b/database/files/pages/ai.llm.analytics.index.yaml
@@ -2,8 +2,8 @@ key: ai.llm.analytics.index
robots: index,follow
image: images/pages/ai.llm.analytics.index.svg
title:
- de_CH: 'LLM-Nutzungsstatistik – codebar Solutions AG'
- en_CH: 'LLM Usage Analytics – codebar Solutions AG'
+ de_CH: 'LLM-Nutzungsstatistik'
+ en_CH: 'LLM Usage Analytics'
description:
de_CH: 'Token-Verbrauch und Anfragen unserer lokal betriebenen Modelle – transparent aufgeschlüsselt pro Monat und Modell.'
en_CH: 'Token consumption and requests of our locally run models – transparently broken down per month and model.'
diff --git a/database/files/pages/ai.llm.index.yaml b/database/files/pages/ai.llm.index.yaml
index 6974ddd..a0c02f5 100644
--- a/database/files/pages/ai.llm.index.yaml
+++ b/database/files/pages/ai.llm.index.yaml
@@ -2,8 +2,8 @@ key: ai.llm.index
robots: index,follow
image: images/pages/ai.llm.index.svg
title:
- de_CH: 'Unsere lokalen LLMs im Einsatz – codebar Solutions AG'
- en_CH: 'Our Local LLMs in Action – codebar Solutions AG'
+ de_CH: 'Unsere lokalen LLMs im Einsatz'
+ en_CH: 'Our Local LLMs in Action'
description:
de_CH: 'Diese lokalen Open-Source-Modelle betreiben wir aktuell – auf eigener Hardware im hauseigenen Bürokeller. Modelle, Infrastruktur und Nutzung.'
en_CH: 'The local open-source models we currently run – on our own hardware, in our own office basement. Models, infrastructure and usage.'
diff --git a/database/files/pages/co-working.index.yaml b/database/files/pages/co-working.index.yaml
index 56c9121..1915a01 100644
--- a/database/files/pages/co-working.index.yaml
+++ b/database/files/pages/co-working.index.yaml
@@ -2,8 +2,8 @@ key: co-working.index
robots: index,follow
image: images/pages/co-working.index.svg
title:
- de_CH: 'Co-Working bei codebar Solutions AG'
- en_CH: 'Co-Working at codebar Solutions AG'
+ de_CH: 'Co-Working bei codebar'
+ en_CH: 'Co-Working at codebar'
description:
de_CH: 'Ein professioneller Arbeitsplatz in Oberwil — ruhig, gut ausgestattet und Teil eines Tech-Teams.'
en_CH: 'A professional workspace in Oberwil — quiet, well-equipped, and embedded in a tech team.'
diff --git a/database/files/pages/contact.index.yaml b/database/files/pages/contact.index.yaml
index 233b930..4e13bbb 100644
--- a/database/files/pages/contact.index.yaml
+++ b/database/files/pages/contact.index.yaml
@@ -2,8 +2,8 @@ key: contact.index
robots: index,follow
image: images/pages/contact.index.svg
title:
- de_CH: "Kontakt – lass uns sprechen | codebar Solutions AG"
- en_CH: "Contact – Let's Talk | codebar Solutions AG"
+ de_CH: "Kontakt – lass uns sprechen"
+ en_CH: "Contact – Let's Talk"
description:
de_CH: 'Erzähl uns von deiner Idee oder deinem Projekt – wir hören zu. Telefon, E-Mail und unsere zwei Standorte in der Region Basel.'
en_CH: "Tell us about your idea or your project – we'll listen. Phone, email and our two locations in the Basel region."
diff --git a/database/files/pages/jobs.index.yaml b/database/files/pages/jobs.index.yaml
index 427a49c..887b2c7 100644
--- a/database/files/pages/jobs.index.yaml
+++ b/database/files/pages/jobs.index.yaml
@@ -2,8 +2,8 @@ key: jobs.index
robots: index,follow
image: images/pages/jobs.index.svg
title:
- de_CH: 'Stellen – arbeiten bei codebar Solutions AG'
- en_CH: 'Jobs – Working at codebar Solutions AG'
+ de_CH: 'Stellen – arbeiten bei codebar'
+ en_CH: 'Jobs – Working at codebar'
description:
de_CH: 'Klein aus Überzeugung: Verantwortung ab Tag eins, offene Technologien und Praktika mitten im Projektalltag. So arbeitest du bei codebar.'
en_CH: 'Small by conviction: responsibility from day one, open technologies and internships inside real projects. This is what working at codebar is like.'
diff --git a/database/files/pages/legal.imprint.index.yaml b/database/files/pages/legal.imprint.index.yaml
index c52970a..0170370 100644
--- a/database/files/pages/legal.imprint.index.yaml
+++ b/database/files/pages/legal.imprint.index.yaml
@@ -2,8 +2,8 @@ key: legal.imprint.index
robots: index,follow
image: images/pages/legal.imprint.index.svg
title:
- de_CH: 'Impressum – codebar Solutions AG'
- en_CH: 'Imprint – codebar Solutions AG'
+ de_CH: 'Impressum'
+ en_CH: 'Imprint'
description:
de_CH: 'Rechtliche Angaben zur codebar Solutions AG: Rechtsform, UID-Nummer, Handelsregistereintrag, Zeichnungsberechtigte sowie Kontaktdaten und Haftungsausschluss.'
en_CH: 'Legal details of codebar Solutions AG: legal form, UID number, commercial register entry, authorised representatives, contact details and disclaimer.'
diff --git a/database/files/pages/legal.privacy.index.yaml b/database/files/pages/legal.privacy.index.yaml
index dac45f9..4533186 100644
--- a/database/files/pages/legal.privacy.index.yaml
+++ b/database/files/pages/legal.privacy.index.yaml
@@ -2,8 +2,8 @@ key: legal.privacy.index
robots: index,follow
image: images/pages/legal.privacy.index.svg
title:
- de_CH: 'Datenschutz – codebar Solutions AG'
- en_CH: 'Privacy – codebar Solutions AG'
+ de_CH: 'Datenschutz'
+ en_CH: 'Privacy'
description:
de_CH: 'Wie wir Personendaten auf codebar.ch bearbeiten: welche Daten anfallen, wofür wir sie nutzen, wie lange wir sie aufbewahren – und welche Rechte du hast.'
en_CH: 'How we process personal data on codebar.ch: what we collect, why, how long we keep it – and the rights you have.'
diff --git a/database/files/pages/legal.terms.index.yaml b/database/files/pages/legal.terms.index.yaml
index b3ddd20..2a4e3b5 100644
--- a/database/files/pages/legal.terms.index.yaml
+++ b/database/files/pages/legal.terms.index.yaml
@@ -2,8 +2,8 @@ key: legal.terms.index
robots: index,follow
image: images/pages/legal.terms.index.svg
title:
- de_CH: 'AGB – codebar Solutions AG'
- en_CH: 'Terms & Conditions – codebar Solutions AG'
+ de_CH: 'AGB'
+ en_CH: 'Terms & Conditions'
description:
de_CH: 'Die Allgemeinen Geschäftsbedingungen der codebar Solutions AG: Angebote, Projektabwicklung, Nutzungsrechte, Haftung und Support.'
en_CH: 'The general terms and conditions of codebar Solutions AG: offers, project delivery, rights of use, liability and support.'
diff --git a/database/files/pages/media.index.yaml b/database/files/pages/media.index.yaml
index 7ffeb33..bca146c 100644
--- a/database/files/pages/media.index.yaml
+++ b/database/files/pages/media.index.yaml
@@ -2,8 +2,8 @@ key: media.index
robots: index,follow
image: images/pages/media.index.svg
title:
- de_CH: 'Medien & Markenassets – codebar Solutions AG'
- en_CH: 'Media & Brand Assets – codebar Solutions AG'
+ de_CH: 'Medien & Markenassets'
+ en_CH: 'Media & Brand Assets'
description:
de_CH: 'Offizielle codebar-Logos für Presse und Partner – farbig, invertiert, schwarz-weiss, als PNG und SVG.'
en_CH: 'Official codebar logos for press and partners – colour, inverted, black and white, as PNG and SVG.'
diff --git a/database/files/pages/network.index.yaml b/database/files/pages/network.index.yaml
index e95314c..cd37f62 100644
--- a/database/files/pages/network.index.yaml
+++ b/database/files/pages/network.index.yaml
@@ -2,8 +2,8 @@ key: network.index
robots: index,follow
image: images/pages/network.index.svg
title:
- de_CH: 'Netzwerk – Partner & Community | codebar Solutions AG'
- en_CH: 'Network – Partners & Community | codebar Solutions AG'
+ de_CH: 'Netzwerk – Partner & Community'
+ en_CH: 'Network – Partners & Community'
description:
de_CH: 'Mit wem wir arbeiten: Projekt-, Software- und Infrastrukturpartner, unser Engagement in der Community – und die Labels, die dahinterstehen.'
en_CH: 'Who we work with: project, software and infrastructure partners, our community engagement – and the labels behind it.'
diff --git a/database/files/pages/network.request.index.yaml b/database/files/pages/network.request.index.yaml
index 6e9c871..a64175e 100644
--- a/database/files/pages/network.request.index.yaml
+++ b/database/files/pages/network.request.index.yaml
@@ -2,8 +2,8 @@ key: network.request.index
robots: noindex,nofollow
image: images/pages/network.request.index.svg
title:
- de_CH: 'Netzwerk-Profil aktualisieren – codebar Solutions AG'
- en_CH: 'Update network profile – codebar Solutions AG'
+ de_CH: 'Netzwerk-Profil aktualisieren'
+ en_CH: 'Update network profile'
description:
de_CH: 'Persönlichen Link anfordern, um das eigene Profil im codebar Netzwerk zu aktualisieren.'
en_CH: 'Request a personal link to update your codebar network profile.'
diff --git a/database/files/pages/news.index.yaml b/database/files/pages/news.index.yaml
index 21ca761..0c262cd 100644
--- a/database/files/pages/news.index.yaml
+++ b/database/files/pages/news.index.yaml
@@ -2,8 +2,8 @@ key: news.index
robots: index,follow
image: images/pages/news.index.svg
title:
- de_CH: 'Neuigkeiten & Insights – codebar Solutions AG'
- en_CH: 'News & Insights – codebar Solutions AG'
+ de_CH: 'Neuigkeiten & Insights'
+ en_CH: 'News & Insights'
description:
de_CH: 'Aktuelle News, Fachbeiträge und Trends rund um Softwareentwicklung, offene Technologien und digitale Innovation von codebar.'
en_CH: 'Latest news and expert insights on software development, open technologies and digital innovation from codebar.'
diff --git a/database/files/pages/open-source.index.yaml b/database/files/pages/open-source.index.yaml
index d62579c..0fd6348 100644
--- a/database/files/pages/open-source.index.yaml
+++ b/database/files/pages/open-source.index.yaml
@@ -2,8 +2,8 @@ key: open-source.index
robots: index,follow
image: images/pages/open-source.index.svg
title:
- de_CH: 'Open-Source-Beiträge – codebar Solutions AG'
- en_CH: 'Open Source Contributions – codebar Solutions AG'
+ de_CH: 'Open-Source-Beiträge'
+ en_CH: 'Open Source Contributions'
description:
de_CH: 'Unsere Beiträge an die Open-Source-Community – Packages, Tools und Libraries, entwickelt und gepflegt von codebar.'
en_CH: 'Our contributions to the open source community – packages, tools and libraries developed and maintained by codebar.'
diff --git a/database/files/pages/products.index.yaml b/database/files/pages/products.index.yaml
index bd82d34..8c8989f 100644
--- a/database/files/pages/products.index.yaml
+++ b/database/files/pages/products.index.yaml
@@ -2,8 +2,8 @@ key: products.index
robots: index,follow
image: images/pages/products.index.svg
title:
- de_CH: 'Nutzerzentrierte Softwarelösungen – codebar Solutions AG'
- en_CH: 'User-Centric Software Solutions – codebar Solutions AG'
+ de_CH: 'Nutzerzentrierte Softwarelösungen'
+ en_CH: 'User-Centric Software Solutions'
description:
de_CH: 'Nutzerzentrierte Softwarelösungen mit echtem Mehrwert – entwickelt mit offenen Technologien und Standards.'
en_CH: 'Our software solutions are built around the needs of users, delivering real value through open technologies and standards.'
diff --git a/database/files/pages/services.index.yaml b/database/files/pages/services.index.yaml
index 0d03957..440ebef 100644
--- a/database/files/pages/services.index.yaml
+++ b/database/files/pages/services.index.yaml
@@ -2,8 +2,8 @@ key: services.index
robots: index,follow
image: images/pages/services.index.svg
title:
- de_CH: 'Konzeption, Software, DMS & ERP | codebar Solutions AG'
- en_CH: 'Concept, Software, DMS & ERP | codebar Solutions AG'
+ de_CH: 'Konzeption, Software, DMS & ERP'
+ en_CH: 'Concept, Software, DMS & ERP'
description:
de_CH: 'Vier Bereiche, ein Weg: von der ersten Idee bis zum laufenden Betrieb – Konzeption, Softwareentwicklung, Dokumentenmanagement und ERP.'
en_CH: 'Four areas, one path: from the first idea to day-to-day operations – concept design, software development, document management and ERP.'
diff --git a/database/files/pages/start.index.yaml b/database/files/pages/start.index.yaml
index 4f06c9e..f5a2038 100644
--- a/database/files/pages/start.index.yaml
+++ b/database/files/pages/start.index.yaml
@@ -2,8 +2,8 @@ key: start.index
robots: index,follow
image: images/pages/start.index.svg
title:
- de_CH: 'Innovative Ideen zum Leben erwecken – codebar Solutions AG'
- en_CH: 'Bringing Innovative Ideas to Life – codebar Solutions AG'
+ de_CH: 'Innovative Ideen zum Leben erwecken'
+ en_CH: 'Bringing Innovative Ideas to Life'
description:
de_CH: 'Wir hören zu, verstehen dein Problem und begleiten dich von der ersten Idee bis zur Software im täglichen Einsatz. Ein kleines Team aus der Region Basel.'
en_CH: 'We listen, understand your problem and stay with you from the first idea to software in daily use. A small team based in the Basel region.'
diff --git a/database/files/pages/technologies.index.yaml b/database/files/pages/technologies.index.yaml
index e4699bc..9643973 100644
--- a/database/files/pages/technologies.index.yaml
+++ b/database/files/pages/technologies.index.yaml
@@ -2,8 +2,8 @@ key: technologies.index
robots: index,follow
image: images/pages/technologies.index.svg
title:
- de_CH: 'Offene Technologien & Standards – codebar Solutions AG'
- en_CH: 'Open Technologies & Standards – codebar Solutions AG'
+ de_CH: 'Offene Technologien & Standards'
+ en_CH: 'Open Technologies & Standards'
description:
de_CH: 'Die offenen Technologien und Standards, auf denen unsere Software basiert – bewährt, transparent und herstellerunabhängig.'
en_CH: 'The open technologies and standards we build our software on – proven, transparent and vendor-independent.'
diff --git a/database/migrations/2025_04_04_175530_create_products_table.php b/database/migrations/2025_04_04_175530_create_products_table.php
index 10d7af9..38d4aee 100644
--- a/database/migrations/2025_04_04_175530_create_products_table.php
+++ b/database/migrations/2025_04_04_175530_create_products_table.php
@@ -34,6 +34,8 @@ public function up(): void
$table->string('url')->nullable();
$table->json('tags')->nullable();
$table->timestamps();
+
+ $table->index(['published', 'order']);
});
}
diff --git a/database/migrations/2025_04_04_175541_create_services_table.php b/database/migrations/2025_04_04_175541_create_services_table.php
index e5108bb..99f0fca 100644
--- a/database/migrations/2025_04_04_175541_create_services_table.php
+++ b/database/migrations/2025_04_04_175541_create_services_table.php
@@ -26,6 +26,8 @@ public function up(): void
$table->string('url')->nullable();
$table->json('tags')->nullable();
$table->timestamps();
+
+ $table->index(['published', 'order']);
});
}
diff --git a/database/migrations/2025_06_23_130054_create_contacts_table.php b/database/migrations/2025_06_23_130054_create_contacts_table.php
index be75c85..71d4a43 100644
--- a/database/migrations/2025_06_23_130054_create_contacts_table.php
+++ b/database/migrations/2025_06_23_130054_create_contacts_table.php
@@ -23,6 +23,8 @@ public function up(): void
$table->string('image');
$table->json('icons')->nullable();
$table->timestamps();
+
+ $table->index(['published', 'sort']);
});
}
diff --git a/database/migrations/2025_06_24_063716_create_technologies_table.php b/database/migrations/2025_06_24_063716_create_technologies_table.php
index 22975ff..0882c34 100644
--- a/database/migrations/2025_06_24_063716_create_technologies_table.php
+++ b/database/migrations/2025_06_24_063716_create_technologies_table.php
@@ -26,6 +26,8 @@ public function up(): void
$table->json('tags')->nullable();
$table->string('link')->nullable();
$table->timestamps();
+
+ $table->index(['published', 'order']);
});
}
diff --git a/database/migrations/2026_07_22_100000_create_networks_table.php b/database/migrations/2026_07_22_100000_create_networks_table.php
index 6df0018..5090a58 100644
--- a/database/migrations/2026_07_22_100000_create_networks_table.php
+++ b/database/migrations/2026_07_22_100000_create_networks_table.php
@@ -27,10 +27,12 @@ public function up(): void
$table->string('website')->nullable();
$table->unsignedSmallInteger('since_year')->nullable();
$table->unsignedSmallInteger('until_year')->nullable();
- $table->string('page_slug')->nullable();
+ $table->string('page_slug')->nullable()->unique();
$table->boolean('published')->default(true);
$table->unsignedInteger('sort')->default(0);
$table->timestamps();
+
+ $table->index(['published', 'status', 'sort']);
});
}
diff --git a/database/migrations/2026_07_22_100010_create_network_users_table.php b/database/migrations/2026_07_22_100010_create_network_users_table.php
index f90be9b..11cb7e6 100644
--- a/database/migrations/2026_07_22_100010_create_network_users_table.php
+++ b/database/migrations/2026_07_22_100010_create_network_users_table.php
@@ -21,7 +21,7 @@ public function up(): void
$table->string('avatar_disk')->nullable();
$table->string('avatar_path')->nullable();
$table->string('avatar_url')->nullable();
- $table->string('email')->nullable()->index();
+ $table->string('email')->nullable()->unique();
$table->string('public_email')->nullable();
$table->string('linkedin')->nullable();
$table->string('phone')->nullable();
diff --git a/database/migrations/2026_07_28_150010_create_news_table.php b/database/migrations/2026_07_28_150010_create_news_table.php
index c207df0..abccb7f 100644
--- a/database/migrations/2026_07_28_150010_create_news_table.php
+++ b/database/migrations/2026_07_28_150010_create_news_table.php
@@ -13,15 +13,12 @@ public function up(): void
{
Schema::create('news', function (Blueprint $table) {
$table->id();
- // Cross-language anchor: the de_CH and en_CH markdown files of one article share it.
$table->string('key')->unique();
$table->json('slug');
$table->json('title');
$table->json('teaser');
$table->json('content')->nullable();
$table->string('hero_image')->nullable();
- // The square the index shows next to a list row. Its own file rather than a crop
- // of the hero: the hero carries the title, and a square crop cuts the words.
$table->string('thumb_image')->nullable();
$table->json('hero_caption')->nullable();
$table->json('hero_alt')->nullable();
@@ -36,12 +33,10 @@ public function up(): void
$table->unsignedSmallInteger('reading_minutes')->nullable();
$table->json('tags')->nullable();
$table->timestamps();
+
+ $table->index(['published', 'published_at']);
});
- // A JSON column cannot carry a plain unique constraint — index the extracted
- // per-locale value instead, so each language keeps its own unique slug.
- // PostgreSQL only: `->>` here takes a bare key, which is not valid MySQL.
- // The whole application targets pgsql (see config/database.php).
foreach (['de_CH', 'en_CH'] as $locale) {
$index = 'news_slug_'.strtolower(str_replace('_', '', $locale)).'_unique';
DB::statement("CREATE UNIQUE INDEX {$index} ON news (( slug->>'{$locale}' ))");
diff --git a/database/migrations/2026_08_01_090000_add_listing_indexes.php b/database/migrations/2026_08_01_090000_add_listing_indexes.php
deleted file mode 100644
index 874c012..0000000
--- a/database/migrations/2026_08_01_090000_add_listing_indexes.php
+++ /dev/null
@@ -1,82 +0,0 @@
-first(), so a duplicate would make
- * the database pick the partner page arbitrarily.
- */
-return new class extends Migration
-{
- public function up(): void
- {
- Schema::table('news', function (Blueprint $table): void {
- $table->index(['published', 'published_at']);
- });
-
- Schema::table('services', function (Blueprint $table): void {
- $table->index(['published', 'order']);
- });
-
- Schema::table('products', function (Blueprint $table): void {
- $table->index(['published', 'order']);
- });
-
- Schema::table('technologies', function (Blueprint $table): void {
- $table->index(['published', 'order']);
- });
-
- Schema::table('contacts', function (Blueprint $table): void {
- $table->index(['published', 'sort']);
- });
-
- Schema::table('networks', function (Blueprint $table): void {
- $table->index(['published', 'status', 'sort']);
- $table->unique('page_slug');
- });
-
- Schema::table('network_users', function (Blueprint $table): void {
- $table->unique('email');
- });
- }
-
- public function down(): void
- {
- Schema::table('news', function (Blueprint $table): void {
- $table->dropIndex(['published', 'published_at']);
- });
-
- Schema::table('services', function (Blueprint $table): void {
- $table->dropIndex(['published', 'order']);
- });
-
- Schema::table('products', function (Blueprint $table): void {
- $table->dropIndex(['published', 'order']);
- });
-
- Schema::table('technologies', function (Blueprint $table): void {
- $table->dropIndex(['published', 'order']);
- });
-
- Schema::table('contacts', function (Blueprint $table): void {
- $table->dropIndex(['published', 'sort']);
- });
-
- Schema::table('networks', function (Blueprint $table): void {
- $table->dropIndex(['published', 'status', 'sort']);
- $table->dropUnique(['page_slug']);
- });
-
- Schema::table('network_users', function (Blueprint $table): void {
- $table->dropUnique(['email']);
- });
- }
-};
diff --git a/database/seeders/NetworkUsersTableSeeder.php b/database/seeders/NetworkUsersTableSeeder.php
index 9f088a3..55f6ed3 100644
--- a/database/seeders/NetworkUsersTableSeeder.php
+++ b/database/seeders/NetworkUsersTableSeeder.php
@@ -10,7 +10,7 @@
/**
* Contact channels are real where known, otherwise placeholder (*@example.com).
- * All rows seed as unpublished — a contact only appears on the network page once
+ * Rows seed unpublished by default — a contact only appears on the network page once
* explicitly published.
*
* Sourced from network_users.csv — the current data export — not hand-authored,
diff --git a/database/seeders/data/network_users.csv b/database/seeders/data/network_users.csv
index 90317b3..2e48026 100644
--- a/database/seeders/data/network_users.csv
+++ b/database/seeders/data/network_users.csv
@@ -1,6 +1,6 @@
id;network_key;name;role;avatar_disk;avatar_path;avatar_url;email;public_email;linkedin;phone;published;sort;created_at;updated_at
-1;wieland-business-solutions;Dario Wieland;;;;;dario.wieland@business-solutions.gmbh;dario.wieland@business-solutions.gmbh;https://www.linkedin.com/in/dario-wieland/;+41 81 560 21 20;false;10;2026-07-23 08:16:38;2026-07-25 10:02:48
-2;pst;Sarah Fässler;;;;;sarah.faessler@pstgmbh.ch;sarah.faessler@pstgmbh.ch;;;false;10;2026-07-23 08:16:38;2026-07-23 08:16:38
+1;wieland-business-solutions;Dario Wieland;;;;;dario.wieland@business-solutions.gmbh;dario.wieland@business-solutions.gmbh;https://www.linkedin.com/in/dario-wieland/;+41 81 560 21 20;true;10;2026-07-23 08:16:38;2026-07-25 10:02:48
+2;pst;Sarah Fässler;;;;;sarah.faessler@pstgmbh.ch;sarah.faessler@pstgmbh.ch;https://www.linkedin.com/in/sarah-f%C3%A4ss%C4%BAer-30492b152/;;true;10;2026-07-23 08:16:38;2026-07-23 08:16:38
3;docuware;Vincenzo Carbone;DocuWare Schweiz;;;/images/placeholders/avatar-sample.svg;vincenzo.carbone@docuware.com;vincenzo.carbone@docuware.com;;;false;10;2026-07-23 08:16:38;2026-07-23 08:16:38
4;odoo;Domenik Friedrich;;;;;domf@odoo.com;domf@odoo.com;;;false;10;2026-07-23 08:16:38;2026-07-23 08:16:38
5;baselhack;BaselHack;;;;;info@baselhack.ch;info@baselhack.ch;;;false;10;2026-07-23 08:16:38;2026-07-23 08:16:38
diff --git a/lang/de_CH/components.php b/lang/de_CH/components.php
index 1c20728..75e1dd6 100644
--- a/lang/de_CH/components.php
+++ b/lang/de_CH/components.php
@@ -7,6 +7,7 @@
'title' => 'Schön, bist du da',
'window' => 'Wir sind codebar',
'legend' => 'Was möchtest du über uns wissen?',
+ 'overview' => 'Übersicht',
'shortcuts' => 'Abschnitt wechseln: Zifferntasten 1 bis 3 oder die Pfeiltasten links und rechts.',
'prev_section' => 'Vorheriger Abschnitt',
'next_section' => 'Nächster Abschnitt',
diff --git a/lang/en_CH/components.php b/lang/en_CH/components.php
index 496fb3e..3fc3bfc 100644
--- a/lang/en_CH/components.php
+++ b/lang/en_CH/components.php
@@ -7,6 +7,7 @@
'title' => 'Good to have you here',
'window' => 'We are codebar',
'legend' => 'What would you like to know about us?',
+ 'overview' => 'Overview',
'shortcuts' => 'Switch section: number keys 1 to 3, or the left and right arrow keys.',
'prev_section' => 'Previous section',
'next_section' => 'Next section',
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 15ee930..da52616 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -42,6 +42,7 @@
+
diff --git a/resources/views/components/intro.blade.php b/resources/views/components/intro.blade.php
index 9112995..4c985ce 100644
--- a/resources/views/components/intro.blade.php
+++ b/resources/views/components/intro.blade.php
@@ -26,7 +26,8 @@
-
+
@foreach(array_slice($sections, 1) as $section)
diff --git a/resources/views/layouts/_partials/_seo.blade.php b/resources/views/layouts/_partials/_seo.blade.php
index c6567de..f5499a7 100644
--- a/resources/views/layouts/_partials/_seo.blade.php
+++ b/resources/views/layouts/_partials/_seo.blade.php
@@ -41,13 +41,8 @@
})
)
->sortKeys();
-
- $brand = config()->string('app.name');
- $documentTitle = $page->isArticle() || str_contains($page->title, $brand)
- ? $page->title
- : $page->title.' – '.$brand;
@endphp
- {{ $documentTitle }}
+ {{ $page->title }}
diff --git a/tests/Feature/Seeders/NetworkSeedersTest.php b/tests/Feature/Seeders/NetworkSeedersTest.php
index bc162c9..8b65930 100644
--- a/tests/Feature/Seeders/NetworkSeedersTest.php
+++ b/tests/Feature/Seeders/NetworkSeedersTest.php
@@ -51,7 +51,7 @@
->and($baselhack->page_slug)->toBeNull();
})->group('network', 'seeders');
-it('seeds unpublished contact persons with real channels where known', function () {
+it('seeds contact persons with real channels where known, published only where flagged', function () {
seed(NetworkUsersTableSeeder::class);
$vincenzo = NetworkUser::where('network_key', 'docuware')->firstOrFail();
@@ -62,13 +62,19 @@
$baselhack = NetworkUser::where('network_key', 'baselhack')->firstOrFail();
expect(NetworkUser::count())->toBe(6)
- ->and(NetworkUser::where('published', true)->count())->toBe(0)
+ ->and($dario->published)->toBeTrue()
+ ->and($sarah->published)->toBeTrue()
+ ->and($vincenzo->published)->toBeFalse()
+ ->and($domenik->published)->toBeFalse()
+ ->and($patrick->published)->toBeFalse()
+ ->and($baselhack->published)->toBeFalse()
->and($vincenzo->name)->toBe('Vincenzo Carbone')
->and($vincenzo->role)->toBe('DocuWare Schweiz')
->and($vincenzo->email)->toBe('vincenzo.carbone@docuware.com')
->and($vincenzo->avatar_url)->toBe('/images/placeholders/avatar-sample.svg')
->and($dario->email)->toBe('dario.wieland@business-solutions.gmbh')
->and($sarah->email)->toBe('sarah.faessler@pstgmbh.ch')
+ ->and($sarah->linkedin)->not->toBeNull()
->and($domenik->name)->toBe('Domenik Friedrich')
->and($domenik->email)->toBe('domf@odoo.com')
->and($patrick->linkedin)->not->toBeNull()
diff --git a/tests/Feature/Seo/MetaTagsTest.php b/tests/Feature/Seo/MetaTagsTest.php
index f43d3c6..a9b53c3 100644
--- a/tests/Feature/Seo/MetaTagsTest.php
+++ b/tests/Feature/Seo/MetaTagsTest.php
@@ -151,14 +151,14 @@ function titleOf(string $html): string
expect(mb_strlen(titleOf($article)))->toBeLessThanOrEqual(60);
})->group('seo');
-it('appends the company name to a page title that does not already carry it', function () {
+it('renders a page title verbatim without appending the company name', function () {
Page::where('key', 'start.index')->update([
'title' => ['de_CH' => 'Ohne Marke im Titel', 'en_CH' => 'Without the brand'],
]);
$html = (string) get(route('de-ch.start.index'))->assertOk()->getContent();
- expect(titleOf($html))->toBe('Ohne Marke im Titel – '.config()->string('app.name'));
+ expect(titleOf($html))->toBe('Ohne Marke im Titel');
})->group('seo');
it('serves a page image as PNG even when the page stores an SVG', function () {
diff --git a/tests/Feature/Seo/StructuredDataTest.php b/tests/Feature/Seo/StructuredDataTest.php
index b21bfff..17cc2e0 100644
--- a/tests/Feature/Seo/StructuredDataTest.php
+++ b/tests/Feature/Seo/StructuredDataTest.php
@@ -117,22 +117,23 @@ function breadcrumbItems(array $breadcrumb): Collection
}
})->group('seo', 'schema');
-it('carries the full company name in every indexable page title', function () {
- // A title without the brand wastes the strongest signal a search result has.
+it('keeps the legal company name out of every page title', function () {
+ // The brand belongs in og:site_name and the Organization node; repeating it in
+ // every title only eats the characters a search result actually shows.
// Collected rather than asserted one by one so a failure names every offender.
- $missing = [];
+ $offenders = [];
- Page::all()->each(function (Page $page) use (&$missing): void {
+ Page::all()->each(function (Page $page) use (&$offenders): void {
foreach (['de_CH', 'en_CH'] as $locale) {
$title = $page->getTranslation('title', $locale);
- if (is_string($title) && $title !== '' && ! str_contains($title, 'codebar Solutions AG')) {
- $missing[] = "{$page->key} [{$locale}]: {$title}";
+ if (is_string($title) && str_contains($title, 'codebar Solutions AG')) {
+ $offenders[] = "{$page->key} [{$locale}]: {$title}";
}
}
});
- expect($missing)->toBe([]);
+ expect($offenders)->toBe([]);
})->group('seo');
it('links WebSite and WebPage back to the organization by id', function () {