diff --git a/app/Actions/ViewDataAction.php b/app/Actions/ViewDataAction.php index b643be8..403b1ad 100644 --- a/app/Actions/ViewDataAction.php +++ b/app/Actions/ViewDataAction.php @@ -81,31 +81,16 @@ public function contacts(string $locale): object ->orderBy('name') ->get(); - $employeeIds = $publishedContacts - ->filter(fn (Contact $contact): bool => array_key_exists( - ContactSectionEnum::EMPLOYEES, - $contact->sections ?? [] - )) - ->pluck('id'); - return (object) collect([ ContactSectionEnum::EMPLOYEES, ContactSectionEnum::COLLABORATIONS, ContactSectionEnum::BOARD_MEMBERS, - ])->mapWithKeys(function (string $section) use ($publishedContacts, $locale, $employeeIds): array { + ])->mapWithKeys(function (string $section) use ($publishedContacts, $locale): array { $contacts = $publishedContacts - ->filter(function (Contact $contact) use ($section, $employeeIds): bool { + ->filter(function (Contact $contact) use ($section): bool { $sections = $contact->sections ?? []; - if (! array_key_exists($section, $sections)) { - return false; - } - - if ($section === ContactSectionEnum::BOARD_MEMBERS && $employeeIds->contains($contact->id)) { - return false; - } - - return true; + return array_key_exists($section, $sections); }) ->map(fn (Contact $contact) => ContactDTO::fromModel($contact, $section, $locale)); diff --git a/app/Http/Controllers/Legal/PrivacyIndexController.php b/app/Http/Controllers/Legal/PrivacyIndexController.php index f07f17b..6fac7b2 100644 --- a/app/Http/Controllers/Legal/PrivacyIndexController.php +++ b/app/Http/Controllers/Legal/PrivacyIndexController.php @@ -4,22 +4,14 @@ use App\Actions\PageAction; use App\Http\Controllers\Controller; -use Illuminate\Http\RedirectResponse; -use Illuminate\Support\Str; use Illuminate\View\View; class PrivacyIndexController extends Controller { - /** - * Display the user's profile form. - */ - public function __invoke(): View|RedirectResponse + public function __invoke(): View { - return redirect()->route(Str::slug(app()->getLocale()).'.start.index'); - - // @todo Notification - /* return view('app.legal.privacy.index')->with([ - 'page' => (new PageAction(locale: null, routeName: 'legal.privacy.index'))->default(), - ]);*/ + return view('app.legal.privacy.index')->with([ + 'page' => (new PageAction(locale: null, routeName: 'legal.privacy.index'))->default(), + ]); } } diff --git a/app/Http/Controllers/Robots/RobotsController.php b/app/Http/Controllers/Robots/RobotsController.php new file mode 100644 index 0000000..bcfc6b5 --- /dev/null +++ b/app/Http/Controllers/Robots/RobotsController.php @@ -0,0 +1,25 @@ + 'text/plain', + ]); + } +} diff --git a/app/Http/Controllers/Sitemap/SitemapController.php b/app/Http/Controllers/Sitemap/SitemapController.php index 43074e6..4245351 100644 --- a/app/Http/Controllers/Sitemap/SitemapController.php +++ b/app/Http/Controllers/Sitemap/SitemapController.php @@ -21,13 +21,13 @@ class SitemapController extends Controller protected const array DEFAULT_ROUTES = [ 'start.index', 'about-us.index', + 'news.index', 'products.index', 'services.index', 'contact.index', - 'legal.terms.index', + 'media.index', 'legal.imprint.index', 'legal.privacy.index', - 'jobs.index', ]; protected const array DEFAULT_LOCALES = [ @@ -37,12 +37,16 @@ class SitemapController extends Controller public function __invoke(): Response { - $content = Cache::rememberForever(key: 'sitemap_xml', callback: function (): string { - $this->sitemap = new SitemapBuilder; - $this->builder(); + $content = Cache::remember( + key: 'sitemap_xml', + ttl: now()->addHours(24), + callback: function (): string { + $this->sitemap = new SitemapBuilder; + $this->builder(); - return $this->sitemap->toXml(); - }); + return $this->sitemap->toXml(); + } + ); return response(content: $content) ->header('Content-Type', 'application/xml') @@ -55,6 +59,7 @@ private function builder(): void // Use chunked queries to prevent memory issues News::whereNotNull('published_at') + ->where('published_at', '<=', now()) ->with('references') ->chunk(100, function (Collection $news): void { /** @var News $item */ diff --git a/app/Models/Contact.php b/app/Models/Contact.php index c90840e..7033486 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -2,8 +2,11 @@ namespace App\Models; +use App\Enums\LocaleEnum; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Str; class Contact extends Model { @@ -14,4 +17,17 @@ class Contact extends Model 'sections' => 'json', 'icons' => 'json', ]; + + protected static function booted(): void + { + static::saved(fn () => self::clearPublishedCache()); + static::deleted(fn () => self::clearPublishedCache()); + } + + public static function clearPublishedCache(): void + { + foreach (LocaleEnum::cases() as $locale) { + Cache::forget(Str::slug("contacts_published_{$locale->value}")); + } + } } diff --git a/app/Sitemap/SitemapBuilder.php b/app/Sitemap/SitemapBuilder.php index 7366917..06fcb0d 100644 --- a/app/Sitemap/SitemapBuilder.php +++ b/app/Sitemap/SitemapBuilder.php @@ -26,7 +26,10 @@ public function addItem(PageDTO $page): void $url->setPriority(priority: 1.0); $url->setChangeFrequency(changeFrequency: Url::CHANGE_FREQUENCY_WEEKLY); $url->setLastModificationDate(lastModificationDate: $this->lastModificationDate); - $url->addImage(url: $page->image, caption: $page->title); + + if (filled($page->image)) { + $url->addImage(url: $page->image, caption: $page->title); + } if (! empty($page->referencePages) && $page->referencePages->count()) { $page->referencePages->each(function (PageDTO $page) use ($url): void { diff --git a/config/csp.php b/config/csp.php index f14b154..468d990 100644 --- a/config/csp.php +++ b/config/csp.php @@ -43,7 +43,7 @@ /* * Headers will only be added if this setting is set to true. */ - 'enabled' => env('CSP_ENABLED', false), + 'enabled' => env('CSP_ENABLED', true), /* * The class responsible for generating the nonces used in inline tags and headers. diff --git a/config/seo.php b/config/seo.php index 590d933..2966398 100644 --- a/config/seo.php +++ b/config/seo.php @@ -1,7 +1,7 @@ 'images/seo/og-codebar.webp', + 'default_image' => 'images/seo/og-codebar.png', 'image_width' => 1200, 'image_height' => 630, ]; diff --git a/database/seeders/Codebar/PagesTableSeeder.php b/database/seeders/Codebar/PagesTableSeeder.php index 516cae0..42a10ca 100644 --- a/database/seeders/Codebar/PagesTableSeeder.php +++ b/database/seeders/Codebar/PagesTableSeeder.php @@ -93,6 +93,18 @@ private function enCH() ] ); + Page::updateOrCreate( + [ + 'key' => 'legal.privacy.index', + 'locale' => $locale, + ], + [ + 'robots' => 'index,follow', + 'title' => 'Privacy Policy – codebar', + 'description' => 'How codebar Solutions AG processes personal data on this website under Swiss data protection law.', + ] + ); + Page::updateOrCreate( [ 'key' => 'contact.index', @@ -194,6 +206,18 @@ private function deCH() ] ); + Page::updateOrCreate( + [ + 'key' => 'legal.privacy.index', + 'locale' => $locale, + ], + [ + 'robots' => 'index,follow', + 'title' => 'Datenschutzerklärung – codebar', + 'description' => 'Wie die codebar Solutions AG Personendaten auf dieser Website gemäss Schweizer Datenschutzrecht bearbeitet.', + ] + ); + Page::updateOrCreate( [ 'key' => 'contact.index', diff --git a/lang/de_CH.json b/lang/de_CH.json index 55bae4d..65fd456 100644 --- a/lang/de_CH.json +++ b/lang/de_CH.json @@ -21,12 +21,21 @@ "Headquarter": "Hauptsitz", "Home": "Start", "Imprint": "Impressum", + "Imprint disclaimer": "Die Inhalte dieser Website wurden mit grösster Sorgfalt zusammengestellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Als Diensteanbieter sind wir für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Wir sind nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.", + "Imprint representatives placeholder": "Angaben zu zeichnungsberechtigten Personen werden in Kürze veröffentlicht.", + "Imprint individual signature": "Einzelunterschrift", + "Imprint Sebastian Burgin residence": "von Arlesheim, in Oberwil (BL)", + "Imprint Sebastian Burgin role": "Präsident des Verwaltungsrates, Geschäftsführer", + "Imprint Melanie Burgin residence": "von Rothenfluh, in Oberwil (BL)", + "Imprint Melanie Burgin role": "Mitglied des Verwaltungsrates", "Info(at)paperflakes.ch": "Info(at)paperflakes.ch", + "info(at)codebar.ch": "info(at)codebar.ch", "Jobs": "Stellen", "Language": "Sprache", "Last updated at": "Zuletzt aktualisiert am", "Last updated at: :date": "Zuletzt aktualisiert am: :date", "Legal": "Rechtliches", + "Legal form AG": "Aktiengesellschaft (AG)", "Locations": "Standorte", "Media": "Medien", "Media intro": "Offizielle codebar-Logos für Presse und Partner.", @@ -44,6 +53,41 @@ "paperflakes AG": "paperflakes AG", "Phone": "Telefon", "Privacy": "Datenschutz", + "Privacy changes body": "Wir können diese Datenschutzerklärung von Zeit zu Zeit anpassen. Die aktuelle Version ist stets auf dieser Seite mit dem Datum der letzten Aktualisierung verfügbar.", + "Privacy changes heading": "Änderungen", + "Privacy contact body": "Bei Fragen zum Datenschutz oder zur Ausübung Ihrer Rechte:", + "Privacy contact body paperflakes": "Bei Fragen zum Datenschutz oder zur Ausübung Ihrer Rechte:", + "Privacy contact heading": "Kontakt", + "Privacy controller body": "Verantwortlich für die Datenbearbeitung auf dieser Website ist die codebar Solutions AG.", + "Privacy controller body paperflakes": "Verantwortlich für die Datenbearbeitung auf dieser Website ist die paperflakes AG.", + "Privacy controller heading": "Verantwortliche Stelle", + "Privacy data collected analytics": "Nutzungsstatistiken über Fathom Analytics (cookieless, anonymisierte Seitenaufrufe; keine Personenprofile)", + "Privacy data collected errors": "Fehler- und Leistungsdaten an Flare zur Fehlerbehebung (serverseitig; sensible Header werden zensiert)", + "Privacy data collected heading": "Welche Daten wir erheben", + "Privacy data collected intro": "Beim Besuch dieser Website verarbeiten wir folgende Datenkategorien:", + "Privacy data collected logs": "Server-Logdaten (IP-Adresse, Datum und Uhrzeit des Zugriffs, Browsertyp, besuchte Seiten)", + "Privacy data collected session": "Technisch notwendige Session- und CSRF-Cookies für den sicheren Betrieb der Website", + "Privacy processors body": "Wir setzen folgende Dienstleister ein, die Personendaten in unserem Auftrag verarbeiten können:", + "Privacy processors cloudinary": "Cloudinary – Auslieferung von Bildern und Medien (Datenschutz: cloudinary.com/privacy)", + "Privacy processors fathom": "Fathom Analytics – datenschutzfreundliche Website-Statistiken (usefathom.com/privacy)", + "Privacy processors flare": "Flare – Fehlerüberwachung unserer Anwendung (flareapp.io/privacy)", + "Privacy processors heading": "Auftragsbearbeiter", + "Privacy processors hosting": "Laravel Cloud – Website-Hosting und Infrastruktur (laravel.com)", + "Privacy purpose body": "Wir verarbeiten diese Daten, um die Website zu betreiben und abzusichern, aggregierte Nutzungsmuster zu verstehen und technische Probleme zu beheben. Rechtsgrundlage ist unser berechtigtes Interesse an einem sicheren und funktionsfähigen Webangebot sowie gegebenenfalls die Vertragserfüllung.", + "Privacy purpose heading": "Zweck und Rechtsgrundlage", + "Privacy retention analytics": "Analysedaten werden gemäss Richtlinie von Fathom aufbewahrt.", + "Privacy retention errors": "Fehlermeldungen werden so lange aufbewahrt, wie zur Behebung erforderlich.", + "Privacy retention heading": "Aufbewahrungsdauer", + "Privacy retention logs": "Server-Logs werden bis zu 90 Tage aufbewahrt.", + "Privacy retention session": "Session-Daten werden beim Schliessen des Browsers gelöscht.", + "Privacy rights body": "Nach dem Schweizer Datenschutzgesetz (nDSG) haben Sie das Recht auf Auskunft, Berichtigung unrichtiger Daten, Löschung soweit anwendbar sowie Widerspruch gegen die Bearbeitung. Wenden Sie sich dazu über unsere Kontaktseite an uns.", + "Privacy rights heading": "Ihre Rechte", + "Privacy scope body": "Diese Datenschutzerklärung gilt für die codebar-Website unter codebar.ch und deren Unterseiten. Sie gilt nicht für verlinkte Websites Dritter.", + "Privacy scope heading": "Geltungsbereich", + "Privacy security body": "Wir schützen Ihre Daten durch HTTPS-Verschlüsselung und branchenübliche Sicherheitsmassnahmen auf unserer Hosting-Infrastruktur.", + "Privacy security heading": "Datensicherheit", + "Authorized representatives": "Zeichnungsberechtigte Personen", + "Disclaimer": "Haftungsausschluss", "Products": "Produkte", "Published at": "Veröffentlicht am", "Published at: :date": "Veröffentlicht am: :date", diff --git a/lang/de_CH/components.php b/lang/de_CH/components.php index 4977d52..798c402 100644 --- a/lang/de_CH/components.php +++ b/lang/de_CH/components.php @@ -22,7 +22,7 @@ ], 'development' => [ 'title' => 'Individuelle Softwareentwicklung', - 'description' => 'Portallösungen, Schnittstellen und Open Source für Ihre Prozesse.', + 'description' => 'Portallösungen, Schnittstellen und Integrationen – mit Open Source im Fokus.', ], 'dms' => [ 'title' => 'DMS/ECM Consulting & Implementation', diff --git a/lang/en_CH.json b/lang/en_CH.json index 36c3409..b2fc169 100644 --- a/lang/en_CH.json +++ b/lang/en_CH.json @@ -19,12 +19,21 @@ "Google Maps": "Google Maps", "Headquarter": "Headquarter", "Imprint": "Imprint", + "Imprint disclaimer": "The content of this website has been compiled with the greatest possible care. Nevertheless, we cannot guarantee the accuracy, completeness or timeliness of the content. As a service provider, we are responsible for our own content on these pages in accordance with general laws. We are not obliged to monitor transmitted or stored third-party information or to investigate circumstances that indicate illegal activity.", + "Imprint representatives placeholder": "Details of authorized representatives will be published here shortly.", + "Imprint individual signature": "Individual signing authority", + "Imprint Sebastian Burgin residence": "from Arlesheim, resident in Oberwil (BL)", + "Imprint Sebastian Burgin role": "Chairman of the Board of Directors and Managing Director", + "Imprint Melanie Burgin residence": "from Rothenfluh, resident in Oberwil (BL)", + "Imprint Melanie Burgin role": "Member of the Board of Directors", "Info(at)paperflakes.ch": "Info(at)paperflakes.ch", + "info(at)codebar.ch": "info(at)codebar.ch", "Jobs": "Jobs", "Language": "Language", "Last updated at": "Last updated at", "Last updated at: :date": "Last updated at: :date", "Legal": "Legal", + "Legal form AG": "Aktiengesellschaft (AG)", "Locations": "Locations", "Media": "Media", "Media intro": "Download official codebar logos for press and partner use.", @@ -42,6 +51,41 @@ "paperflakes AG": "paperflakes AG", "Phone": "Phone", "Privacy": "Privacy", + "Privacy changes body": "We may update this privacy policy from time to time. The current version is always available on this page with the date of the last update.", + "Privacy changes heading": "Changes to this policy", + "Privacy contact body": "For questions about data protection or to exercise your rights:", + "Privacy contact body paperflakes": "For questions about data protection or to exercise your rights:", + "Privacy contact heading": "Contact", + "Privacy controller body": "The controller responsible for data processing on this website is codebar Solutions AG.", + "Privacy controller body paperflakes": "The controller responsible for data processing on this website is paperflakes AG.", + "Privacy controller heading": "Controller", + "Privacy data collected analytics": "Usage analytics via Fathom Analytics (cookieless, anonymized page views; no personal profiles)", + "Privacy data collected errors": "Error and performance data sent to Flare for troubleshooting (server-side; sensitive headers are censored)", + "Privacy data collected heading": "Data we collect", + "Privacy data collected intro": "When you visit this website, we process the following categories of data:", + "Privacy data collected logs": "Server log data (IP address, date and time of access, browser type, pages visited)", + "Privacy data collected session": "Technically necessary session and CSRF cookies for secure operation of the website", + "Privacy processors body": "We use the following third-party services that may process personal data on our behalf:", + "Privacy processors cloudinary": "Cloudinary – delivery of images and media assets (privacy policy: cloudinary.com/privacy)", + "Privacy processors fathom": "Fathom Analytics – privacy-friendly website statistics (usefathom.com/privacy)", + "Privacy processors flare": "Flare – error monitoring for our application (flareapp.io/privacy)", + "Privacy processors heading": "Third-party processors", + "Privacy processors hosting": "Laravel Cloud – website hosting and infrastructure (laravel.com)", + "Privacy purpose body": "We process this data to operate and secure the website, to understand aggregate usage patterns, and to diagnose technical issues. The legal basis is our legitimate interest in providing a secure and functional website, and where applicable the performance of a contract.", + "Privacy purpose heading": "Purpose and legal basis", + "Privacy retention analytics": "Analytics data is retained according to Fathom's policy.", + "Privacy retention errors": "Error reports are retained as long as necessary to resolve the issue.", + "Privacy retention heading": "Retention", + "Privacy retention logs": "Server logs are retained for up to 90 days.", + "Privacy retention session": "Session data is deleted when you close your browser.", + "Privacy rights body": "Under the Swiss Federal Act on Data Protection (nDSG), you have the right to request information about your personal data, to have inaccurate data corrected, to request deletion where applicable, and to object to processing. To exercise these rights, please use our contact page.", + "Privacy rights heading": "Your rights", + "Privacy scope body": "This privacy policy applies to the codebar website at codebar.ch and its subpages. It does not apply to third-party websites linked from this site.", + "Privacy scope heading": "Scope", + "Privacy security body": "We protect your data using HTTPS encryption and industry-standard security measures on our hosting infrastructure.", + "Privacy security heading": "Data security", + "Authorized representatives": "Authorized representatives", + "Disclaimer": "Disclaimer", "Products": "Products", "Published at": "Published at", "Published at: :date": "Published at: :date", diff --git a/lang/en_CH/components.php b/lang/en_CH/components.php index 81d5903..4c44a69 100644 --- a/lang/en_CH/components.php +++ b/lang/en_CH/components.php @@ -20,7 +20,7 @@ ], 'development' => [ 'title' => 'Individual software development', - 'description' => 'Portal solutions, interfaces and open source for your processes.', + 'description' => 'Portal solutions, interfaces and integrations – with a focus on open source.', ], 'dms' => [ 'title' => 'DMS/ECM consulting & implementation', diff --git a/public/favicons/codebar/site.webmanifest b/public/favicons/codebar/site.webmanifest index ccf313a..bc40922 100644 --- a/public/favicons/codebar/site.webmanifest +++ b/public/favicons/codebar/site.webmanifest @@ -1,15 +1,15 @@ { - "name": "MyWebSite", - "short_name": "MySite", + "name": "codebar Solutions AG", + "short_name": "codebar", "icons": [ { - "src": "/web-app-manifest-192x192.png", + "src": "web-app-manifest-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/web-app-manifest-512x512.png", + "src": "web-app-manifest-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" @@ -18,4 +18,4 @@ "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" -} \ No newline at end of file +} diff --git a/public/favicons/paperflakes/site.webmanifest b/public/favicons/paperflakes/site.webmanifest index ccf313a..61b50ab 100644 --- a/public/favicons/paperflakes/site.webmanifest +++ b/public/favicons/paperflakes/site.webmanifest @@ -1,15 +1,15 @@ { - "name": "MyWebSite", - "short_name": "MySite", + "name": "paperflakes AG", + "short_name": "paperflakes", "icons": [ { - "src": "/web-app-manifest-192x192.png", + "src": "web-app-manifest-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/web-app-manifest-512x512.png", + "src": "web-app-manifest-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" @@ -18,4 +18,4 @@ "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" -} \ No newline at end of file +} diff --git a/public/images/logos/codebar-logo-black-white.svg b/public/images/logos/codebar-logo-black-white.svg index 51e9978..3e0d146 100644 --- a/public/images/logos/codebar-logo-black-white.svg +++ b/public/images/logos/codebar-logo-black-white.svg @@ -1,6 +1,9 @@ - - - - - + + + + + + + + diff --git a/public/images/logos/codebar-logo-colored-inverted.svg b/public/images/logos/codebar-logo-colored-inverted.svg index 57c4834..2f6708d 100644 --- a/public/images/logos/codebar-logo-colored-inverted.svg +++ b/public/images/logos/codebar-logo-colored-inverted.svg @@ -1,15 +1,16 @@ - - - + + + + + - - - - + + + + + - diff --git a/public/images/logos/codebar-logo-colored.svg b/public/images/logos/codebar-logo-colored.svg index f653e46..6d21a22 100644 --- a/public/images/logos/codebar-logo-colored.svg +++ b/public/images/logos/codebar-logo-colored.svg @@ -1,19 +1,16 @@ - - - - + + + + - - - - - - + + + + + - diff --git a/public/images/logos/codebar-logo-white-black.svg b/public/images/logos/codebar-logo-white-black.svg index 3f4b222..dd4c7a5 100644 --- a/public/images/logos/codebar-logo-white-black.svg +++ b/public/images/logos/codebar-logo-white-black.svg @@ -1,4 +1,7 @@ - - - + + + + + + diff --git a/public/images/seo/og-codebar.png b/public/images/seo/og-codebar.png index bcb6234..37b0d51 100644 Binary files a/public/images/seo/og-codebar.png and b/public/images/seo/og-codebar.png differ diff --git a/public/images/seo/og-codebar.svg b/public/images/seo/og-codebar.svg index 8edf6c3..d725ac7 100644 --- a/public/images/seo/og-codebar.svg +++ b/public/images/seo/og-codebar.svg @@ -1,50 +1,58 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + - - + + - - - - - - + + + - - - - - - - - - - - - - - - + + + - + + + - - - + + + + + diff --git a/public/images/seo/og-codebar.webp b/public/images/seo/og-codebar.webp index 761400d..0293565 100644 Binary files a/public/images/seo/og-codebar.webp and b/public/images/seo/og-codebar.webp differ diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index eb05362..0000000 --- a/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: diff --git a/resources/views/app/legal/imprint/_partials/_codebar.blade.php b/resources/views/app/legal/imprint/_partials/_codebar.blade.php index 4ba7910..b285022 100644 --- a/resources/views/app/legal/imprint/_partials/_codebar.blade.php +++ b/resources/views/app/legal/imprint/_partials/_codebar.blade.php @@ -1,10 +1,10 @@ +

codebar Solutions AG

-

Mühlematten 12

-

CH-4455 Zunzgen

+

{{ __('Legal form AG') }}

CHE-257.955.682

@@ -12,4 +12,30 @@ - \ No newline at end of file + + + +
+ + +
+
+ + +
+
+ + + + +
    +
  • Sebastian Bürgin
  • +
  • Melanie Sabrina Bürgin
  • +
+
+
+ + + +

{{ __('Imprint disclaimer') }}

+
diff --git a/resources/views/app/legal/imprint/_partials/_paperflakes.blade.php b/resources/views/app/legal/imprint/_partials/_paperflakes.blade.php index a60e9cd..bd005f0 100644 --- a/resources/views/app/legal/imprint/_partials/_paperflakes.blade.php +++ b/resources/views/app/legal/imprint/_partials/_paperflakes.blade.php @@ -1,10 +1,10 @@ +

paperflakes AG

-

Mühlematten 12

-

CH-4455 Zunzgen

+

{{ __('Legal form AG') }}

CHE-432.585.498

@@ -12,4 +12,25 @@ -
\ No newline at end of file + + + +
+ + +
+
+ + +
+
+ + + +

{{ __('Imprint representatives placeholder') }}

+
+ + + +

{{ __('Imprint disclaimer') }}

+
diff --git a/resources/views/app/legal/privacy/_partials/_codebar.blade.php b/resources/views/app/legal/privacy/_partials/_codebar.blade.php index 77edf1e..93ad04c 100644 --- a/resources/views/app/legal/privacy/_partials/_codebar.blade.php +++ b/resources/views/app/legal/privacy/_partials/_codebar.blade.php @@ -1 +1,80 @@ - \ No newline at end of file + + +

{{ __('Last updated at: :date', ['date' => '2026-06-30']) }}

+ + + + +

{{ __('Privacy controller body') }}

+
+
+ + + + +

{{ __('Privacy scope body') }}

+
+
+ + + + +

{{ __('Privacy data collected intro') }}

+
    +
  • {{ __('Privacy data collected logs') }}
  • +
  • {{ __('Privacy data collected session') }}
  • +
  • {{ __('Privacy data collected analytics') }}
  • +
  • {{ __('Privacy data collected errors') }}
  • +
+
+
+ + + + +

{{ __('Privacy purpose body') }}

+
+
+ + + + +
    +
  • {{ __('Privacy retention session') }}
  • +
  • {{ __('Privacy retention logs') }}
  • +
  • {{ __('Privacy retention analytics') }}
  • +
  • {{ __('Privacy retention errors') }}
  • +
+
+
+ + + + +

{{ __('Privacy rights body') }}

+
+
+ + + + +

{{ __('Privacy security body') }}

+
+
+ + + + +

{{ __('Privacy changes body') }}

+
+
+ + + + +

+ {{ __('Privacy contact body') }} + +

+
+
diff --git a/resources/views/app/legal/privacy/_partials/_paperflakes.blade.php b/resources/views/app/legal/privacy/_partials/_paperflakes.blade.php index 77edf1e..9ac9679 100644 --- a/resources/views/app/legal/privacy/_partials/_paperflakes.blade.php +++ b/resources/views/app/legal/privacy/_partials/_paperflakes.blade.php @@ -1 +1,80 @@ - \ No newline at end of file + + +

{{ __('Last updated at: :date', ['date' => '2026-06-30']) }}

+ + + + +

{{ __('Privacy controller body paperflakes') }}

+
+
+ + + + +

{{ __('Privacy scope body') }}

+
+
+ + + + +

{{ __('Privacy data collected intro') }}

+
    +
  • {{ __('Privacy data collected logs') }}
  • +
  • {{ __('Privacy data collected session') }}
  • +
  • {{ __('Privacy data collected analytics') }}
  • +
  • {{ __('Privacy data collected errors') }}
  • +
+
+
+ + + + +

{{ __('Privacy purpose body') }}

+
+
+ + + + +
    +
  • {{ __('Privacy retention session') }}
  • +
  • {{ __('Privacy retention logs') }}
  • +
  • {{ __('Privacy retention analytics') }}
  • +
  • {{ __('Privacy retention errors') }}
  • +
+
+
+ + + + +

{{ __('Privacy rights body') }}

+
+
+ + + + +

{{ __('Privacy security body') }}

+
+
+ + + + +

{{ __('Privacy changes body') }}

+
+
+ + + + +

+ {{ __('Privacy contact body paperflakes') }} + +

+
+
diff --git a/resources/views/app/media/index.blade.php b/resources/views/app/media/index.blade.php index 308e3cc..6e38dd0 100644 --- a/resources/views/app/media/index.blade.php +++ b/resources/views/app/media/index.blade.php @@ -11,9 +11,13 @@ @foreach($logos as $logo)
-
+
$logo['slug'] === 'codebar-logo-colored-inverted', + 'bg-white' => $logo['slug'] !== 'codebar-logo-colored-inverted', + ])> {{ $logo['label'] }} diff --git a/resources/views/components/legal/prose.blade.php b/resources/views/components/legal/prose.blade.php new file mode 100644 index 0000000..8d5cde0 --- /dev/null +++ b/resources/views/components/legal/prose.blade.php @@ -0,0 +1,5 @@ +@props(['classAttributes' => '']) + +
merge(['class' => trim('prose prose-gray max-w-none text-gray-800 prose-p:my-0 prose-p:leading-relaxed prose-ul:mt-4 prose-ul:mb-0 prose-li:my-1.5 prose-li:leading-relaxed '.$classAttributes)]) }}> + {{ $slot }} +
diff --git a/resources/views/layouts/_logos/_codebar.blade.php b/resources/views/layouts/_logos/_codebar.blade.php index 7612bdd..e66aea6 100644 --- a/resources/views/layouts/_logos/_codebar.blade.php +++ b/resources/views/layouts/_logos/_codebar.blade.php @@ -1,18 +1,7 @@ - - - - - - - - - +codebar diff --git a/resources/views/layouts/_logos/_paperflakes.blade.php b/resources/views/layouts/_logos/_paperflakes.blade.php index da264c1..4a3f9cd 100644 --- a/resources/views/layouts/_logos/_paperflakes.blade.php +++ b/resources/views/layouts/_logos/_paperflakes.blade.php @@ -1,6 +1,6 @@ diff --git a/resources/views/layouts/_partials/_favicons.blade.php b/resources/views/layouts/_partials/_favicons.blade.php index a66588e..b478894 100644 --- a/resources/views/layouts/_partials/_favicons.blade.php +++ b/resources/views/layouts/_partials/_favicons.blade.php @@ -1,19 +1,23 @@ @props(['manifest' => asset('manifest.json'), 'path' => asset('favicons'), 'color' => '#ffffff']) @php - $prefix = match($configuration?->key) { + $prefix = match ($configuration?->key) { '_paperflakes' => 'paperflakes', '_codebar' => 'codebar', - default => $configuration?->key + default => filled($configuration?->key) + ? ltrim((string) $configuration->key, '_') + : 'codebar', }; + + $faviconPath = rtrim($path, '/')."/{$prefix}"; @endphp - - - - - + + + + + - + diff --git a/resources/views/layouts/_partials/_footer.blade.php b/resources/views/layouts/_partials/_footer.blade.php index 6f5afc9..6df1c8b 100644 --- a/resources/views/layouts/_partials/_footer.blade.php +++ b/resources/views/layouts/_partials/_footer.blade.php @@ -39,6 +39,31 @@ classAttributes="text-lg"/>
@endif +
+
+

{{ __('Legal') }}

+
    +
  • + +
  • +
  • + +
  • +
+
+
+

{{ __('Media') }}

+
    +
  • + +
  • +
+
+
+
@include('layouts._partials._footer.labels')
diff --git a/resources/views/layouts/_partials/_navigation.blade.php b/resources/views/layouts/_partials/_navigation.blade.php index e34af21..e6e584f 100644 --- a/resources/views/layouts/_partials/_navigation.blade.php +++ b/resources/views/layouts/_partials/_navigation.blade.php @@ -2,7 +2,7 @@
@if(filled($configuration?->key)) - + @include("layouts._logos.{$configuration->key}") @endif diff --git a/resources/views/layouts/_partials/_seo.blade.php b/resources/views/layouts/_partials/_seo.blade.php index b827883..7baca05 100644 --- a/resources/views/layouts/_partials/_seo.blade.php +++ b/resources/views/layouts/_partials/_seo.blade.php @@ -24,6 +24,8 @@ + + diff --git a/routes/web.php b/routes/web.php index b53da8f..d6c97c8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -16,6 +16,7 @@ use App\Http\Controllers\OpenSource\OpenSourceIndexController; use App\Http\Controllers\Products\ProductsIndexController; use App\Http\Controllers\Products\ProductsShowController; +use App\Http\Controllers\Robots\RobotsController; use App\Http\Controllers\Services\ServicesIndexController; use App\Http\Controllers\Services\ServicesShowController; use App\Http\Controllers\Sitemap\SitemapController; @@ -86,6 +87,7 @@ Route::post('language/update', LocaleUpdateController::class)->name('language.update'); +Route::get('robots.txt', RobotsController::class); Route::get('sitemap.xml', SitemapController::class); require __DIR__.'/well-known.php'; diff --git a/scripts/generate-og-variations.py b/scripts/generate-og-variations.py new file mode 100644 index 0000000..8ff3763 --- /dev/null +++ b/scripts/generate-og-variations.py @@ -0,0 +1,499 @@ +#!/usr/bin/env python3 +"""Generate 25 UI/UX-led OG image variations with fixed inverted logo.""" + +from __future__ import annotations + +import argparse +import subprocess +from pathlib import Path + +W, H = 1200, 630 +ROOT = Path(__file__).resolve().parent.parent +OUT = ROOT / "public/images/seo/variations" + +LOGO_PATH = ( + "M42.025 115.575C17.7 115.575 0.9 98.075 0.9 73.75C0.9 49.425 17.7 32.1 42.025 32.1C56.2 32.1 " + "67.75 38.4 74.75 48.375L59.175 60.45C56.375 57.125 51.475 52.75 42.55 52.75C30.475 52.75 22.25 " + "61.675 22.25 73.75C22.25 85.825 30.475 94.75 42.55 94.75C51.475 94.75 56.2 91.075 59.175 87.4L74.75 " + "99.125C67.75 109.275 56.2 115.575 42.025 115.575ZM121.74 115.75C97.4152 115.75 80.2652 97.725 " + "80.2652 73.75C80.2652 49.425 97.4152 31.925 121.74 31.925C146.24 31.925 163.215 49.425 163.215 " + "73.75C163.215 97.725 146.24 115.75 121.74 115.75ZM121.74 94.75C133.815 94.75 141.69 85.825 141.69 " + "73.75C141.69 61.675 133.815 52.75 121.74 52.75C109.84 52.75 101.79 61.675 101.79 73.75C101.79 " + "85.825 109.84 94.75 121.74 94.75ZM210.256 115.4C188.206 115.4 172.106 97.725 172.106 73.75C172.106 " + "49.425 188.206 32.275 210.256 32.275C220.406 32.275 227.931 35.95 233.356 42.075V0.249994H255.231V114H233.356V105.6C227.931 " + "111.725 220.406 115.4 210.256 115.4ZM213.406 94.75C225.481 94.75 233.356 85.825 233.356 73.75C233.356 " + "61.675 225.481 52.75 213.406 52.75C201.331 52.75 193.281 61.675 193.281 73.75C193.281 85.825 201.331 " + "94.75 213.406 94.75ZM308.144 115.75C283.469 115.75 267.194 97.725 267.194 73.75C267.194 49.775 282.944 " + "31.925 307.269 31.925C331.244 31.925 345.944 49.775 345.944 73.75V79.7H288.194C290.294 90.55 298.169 " + "97.025 308.144 97.025C318.819 97.025 324.419 92.125 327.219 88.45L342.094 99.3C335.094 109.45 322.844 " + "115.75 308.144 115.75ZM288.544 65.875H325.469C323.544 56.425 317.594 49.25 307.269 49.25C297.294 49.25 " + "290.644 55.725 288.544 65.875ZM402.883 115.4C392.733 115.4 385.208 111.725 379.783 105.6V114H357.908V0.249994H379.783V42.075C385.208 " + "35.95 392.733 32.275 402.883 32.275C424.933 32.275 441.033 49.425 441.033 73.75C441.033 97.725 424.933 115.4 " + "402.883 115.4ZM399.733 94.75C411.808 94.75 419.858 85.825 419.858 73.75C419.858 61.675 411.808 52.75 399.733 " + "52.75C387.658 52.75 379.783 61.675 379.783 73.75C379.783 85.825 387.658 94.75 399.733 94.75ZM488.171 115.4C466.121 " + "115.4 450.021 97.725 450.021 73.75C450.021 49.425 466.121 32.275 488.171 32.275C498.321 32.275 505.846 35.95 511.271 " + "42.075V33.5H533.146V114H511.271V105.6C505.846 111.725 498.321 115.4 488.171 115.4ZM491.321 94.75C503.396 94.75 511.271 " + "85.825 511.271 73.75C511.271 61.675 503.396 52.75 491.321 52.75C479.246 52.75 471.196 61.675 471.196 73.75C471.196 " + "85.825 479.246 94.75 491.321 94.75ZM548.084 114V33.5H569.959V48.025C576.784 37 587.634 32.45 598.309 32.275V52.75C590.434 " + "52.925 569.959 54.15 569.959 77.075V114H548.084Z" +) + + +def logo_block(cx: int = 600, cy: int = 315, scale: float = 1.28, bar_fill: str = "#09090b") -> str: + return f""" + + + + + + + """ + + +BASE_DEFS = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + +# (slug, title, extra_defs, background_layers, logo_override or None) +RECIPES: list[tuple[str, str, str, str, str | None]] = [ + ( + "v01-mega-strip", + "Giant skewed stripe", + "", + """ + + + + """, + None, + ), + ( + "v02-strip-stack", + "Strip rhythm", + "", + """ + + + + + + + + + + """, + None, + ), + ( + "v03-strip-slice", + "Stripe crop", + "", + """ + + + + + + """, + None, + ), + ( + "v04-gradient-edge", + "Edge bleed", + "", + """ + + + """, + None, + ), + ( + "v05-dual-strip-cross", + "Crossing strips", + "", + """ + + + """, + None, + ), + ( + "v06-swiss-grid", + "Visible grid", + "", + """ + + + {"".join(f'' for x in range(0, 1201, 100))} + {"".join(f'' for y in range(0, 631, 90))} + """, + None, + ), + ( + "v07-golden-rules", + "Typographic rules", + "", + """ + + + + """, + None, + ), + ( + "v08-pipe-rhythm", + "Pipe dividers", + "", + """ + + + | + | + | + | + """, + None, + ), + ( + "v09-margin-note", + "Asymmetric margin", + "", + """ + + """, + logo_block(cx=700, cy=315), + ), + ( + "v10-index-card", + "List-card float", + "", + """ + + + + """, + None, + ), + ( + "v11-midnight-bar", + "Full dark canvas", + "", + """""", + logo_block(bar_fill="#09090b"), + ), + ( + "v12-navy-swiss", + "Brand navy field", + "", + """""", + logo_block(bar_fill="#152044"), + ), + ( + "v13-spotlight", + "Stage light", + "", + """ + + + """, + None, + ), + ( + "v14-inverted-world", + "Negative space bar", + "", + """ + + """, + logo_block(bar_fill="#09090b"), + ), + ( + "v15-split-tone", + "Half dark half light", + "", + """ + + """, + logo_block(bar_fill="#09090b"), + ), + ( + "v16-paper-grain", + "Printed paper", + "", + """ + + """, + None, + ), + ( + "v17-dither-fade", + "Ordered dither", + "", + """ + + """, + None, + ), + ( + "v18-frosted-glass", + "Frosted panel", + "", + """ + + + + + """, + None, + ), + ( + "v19-mesh-aurora", + "Mesh gradient", + "", + """ + """, + None, + ), + ( + "v20-concrete-minimal", + "Brutalist Swiss", + "", + """ + + """, + None, + ), + ( + "v21-listen-wave", + "Single waveform", + "", + """ + + + """, + None, + ), + ( + "v22-ripple", + "Concentric ripples", + "", + """ + + + + + """, + None, + ), + ( + "v23-orbit-dot", + "Satellite dot", + "", + """ + + + """, + None, + ), + ( + "v24-portal-ring", + "Gradient ring", + "", + """ + + + """, + None, + ), + ( + "v25-horizon-sun", + "Rising arc", + "", + """ + + """, + None, + ), +] + +EXTRA_DEFS = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + + +def build_svg(extra_defs: str, body: str, logo: str) -> str: + return f""" + + +{BASE_DEFS} +{EXTRA_DEFS} +{extra_defs} + +{body} +{logo} + +""" + + +def export_png(svg_path: Path, png_path: Path) -> None: + subprocess.run( + [ + "magick", + "-background", + "none", + "-density", + "200", + str(svg_path), + "-resize", + f"{W}x{H}!", + str(png_path), + ], + check=True, + capture_output=True, + ) + + +def write_index(recipes: list[tuple[str, str, str, str, str | None]]) -> None: + cells = [] + for slug, title, *_ in recipes: + cells.append(f""" + + {title} + {slug.replace('v', 'V').replace('-', ' ')} + {title} + """) + + html = f""" + + + + + codebar OG variations + + + +

codebar OG image variations

+

25 UI/UX concepts with the same inverted logo. Click to open full size, then tell us your pick (e.g. v11-midnight-bar) to set as the live SEO image.

+
{''.join(cells)} +
+ + +""" + (OUT / "index.html").write_text(html, encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate codebar OG variations") + parser.add_argument("--export", action="store_true", help="Export PNGs via ImageMagick") + args = parser.parse_args() + + OUT.mkdir(parents=True, exist_ok=True) + + # v19 mesh uses layered rects + mesh_layers = """ + + + + """ + + for slug, title, extra_defs, body, logo_override in RECIPES: + if slug == "v19-mesh-aurora": + body = mesh_layers + + logo = logo_override if logo_override else logo_block() + svg = build_svg(extra_defs, body, logo) + svg_path = OUT / f"og-codebar-{slug}.svg" + svg_path.write_text(svg, encoding="utf-8") + print(f"Wrote {svg_path.name} — {title}") + + if args.export: + png_path = OUT / f"og-codebar-{slug}.png" + export_png(svg_path, png_path) + print(f" → {png_path.name}") + + write_index(RECIPES) + print("Wrote index.html") + + +if __name__ == "__main__": + main() diff --git a/scripts/promote-og-variation.sh b/scripts/promote-og-variation.sh new file mode 100755 index 0000000..b2e1659 --- /dev/null +++ b/scripts/promote-og-variation.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Promote a variation to the live SEO image. +# Usage: ./scripts/promote-og-variation.sh v11-midnight-bar + +set -euo pipefail + +SLUG="${1:-}" +if [[ -z "$SLUG" ]]; then + echo "Usage: $0 " + echo "Example: $0 v11-midnight-bar" + exit 1 +fi + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +SRC="$ROOT/public/images/seo/variations/og-codebar-${SLUG}.svg" + +if [[ ! -f "$SRC" ]]; then + echo "Not found: $SRC" + exit 1 +fi + +cp "$SRC" "$ROOT/public/images/seo/og-codebar.svg" +magick -background none -density 200 "$ROOT/public/images/seo/og-codebar.svg" -resize 1200x630! "$ROOT/public/images/seo/og-codebar.png" +magick "$ROOT/public/images/seo/og-codebar.png" -quality 92 "$ROOT/public/images/seo/og-codebar.webp" + +echo "Promoted og-codebar-${SLUG} → public/images/seo/og-codebar.{svg,png,webp}" diff --git a/tests/Feature/Controllers/RobotsControllerTest.php b/tests/Feature/Controllers/RobotsControllerTest.php new file mode 100644 index 0000000..9a624e4 --- /dev/null +++ b/tests/Feature/Controllers/RobotsControllerTest.php @@ -0,0 +1,17 @@ + 'https://codebar.ch']); + + $response = get('robots.txt'); + + $response->assertOk(); + $response->assertHeader('Content-Type', 'text/plain; charset=UTF-8'); + + $content = $response->getContent(); + + expect($content)->toContain('User-agent: *'); + expect($content)->toContain('Sitemap: https://codebar.ch/sitemap.xml'); +})->group('robots'); diff --git a/tests/Feature/Controllers/RouteStatusTest.php b/tests/Feature/Controllers/RouteStatusTest.php index f5bc2b6..fda1ac9 100644 --- a/tests/Feature/Controllers/RouteStatusTest.php +++ b/tests/Feature/Controllers/RouteStatusTest.php @@ -17,6 +17,8 @@ [LocaleEnum::EN->value, 'products.index'], // [LocaleEnum::EN->value, 'products.show', ['product' => 1]], [LocaleEnum::EN->value, 'legal.imprint.index'], + [LocaleEnum::EN->value, 'legal.privacy.index'], + [LocaleEnum::EN->value, 'media.index'], [LocaleEnum::EN->value, 'contact.index'], // DE-CH @@ -28,6 +30,8 @@ [LocaleEnum::DE->value, 'products.index'], // [LocaleEnum::DE->value, 'products.show', ['product' => 1]], [LocaleEnum::DE->value, 'legal.imprint.index'], + [LocaleEnum::DE->value, 'legal.privacy.index'], + [LocaleEnum::DE->value, 'media.index'], [LocaleEnum::DE->value, 'contact.index'], ]; }); diff --git a/tests/Feature/Controllers/SitemapControllerTest.php b/tests/Feature/Controllers/SitemapControllerTest.php index 5a2f2bd..d84793b 100644 --- a/tests/Feature/Controllers/SitemapControllerTest.php +++ b/tests/Feature/Controllers/SitemapControllerTest.php @@ -1,5 +1,7 @@ assertHeader('Content-Type', 'application/xml'); expect($response->getContent())->toBe('fake-xml-response'); })->group('sitemap'); + +it('includes live legal and media pages in the sitemap', function () { + $this->seed([ + ConfigurationsTableSeeder::class, + PagesTableSeeder::class, + ]); + + Cache::flush(); + + $response = get('sitemap.xml'); + + $response->assertOk(); + + $content = $response->getContent(); + + expect($content)->toContain('legal/imprint'); + expect($content)->toContain('legal/privacy'); + expect($content)->toContain('media'); + expect($content)->toContain('rechtlichtes/impressum'); + expect($content)->toContain('rechtlichtes/datenschutz'); + expect($content)->toContain('medien'); +})->group('sitemap'); diff --git a/tests/Feature/Http/SecurityHeadersTest.php b/tests/Feature/Http/SecurityHeadersTest.php index da6ed31..7c0192b 100644 --- a/tests/Feature/Http/SecurityHeadersTest.php +++ b/tests/Feature/Http/SecurityHeadersTest.php @@ -6,8 +6,6 @@ use function Pest\Laravel\get; it('adds security headers on public pages', function () { - config(['csp.enabled' => true]); - $response = get(route(Str::slug(LocaleEnum::DE->value).'.start.index')); $response->assertOk(); @@ -15,14 +13,6 @@ $response->assertHeader('X-Content-Type-Options', 'nosniff'); $response->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); $response->assertHeader('X-Frame-Options', 'DENY'); -})->group('security'); - -it('adds content security policy when enabled', function () { - config(['csp.enabled' => true]); - - $response = get(route(Str::slug(LocaleEnum::DE->value).'.start.index')); - - $response->assertOk(); expect($response->headers->get('Content-Security-Policy'))->toContain("frame-ancestors 'self'"); })->group('security'); diff --git a/todo.md b/todo.md index f4a8337..6c315af 100644 --- a/todo.md +++ b/todo.md @@ -4,9 +4,9 @@ -[ ] About Us -[ ] Breadcrumbs Website -[ ] Contact: Add Social Media LinkedIn --[ ] Legal Privacy +-[x] Legal Privacy -[ ] Legal Terms --[ ] Media Page +-[x] Media Page -[ ] Custom 404 -[ ] Flash Notifications: Language Switcher