Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,17 @@ We have added some blade components which are located in the `resources/views/co

You should include both the blade components in your blade layout files

## Laravel Cloud deployment

When deploying to Laravel Cloud, ensure these environment variables are set for Lighthouse Best Practices (security headers):

- `CSP_ENABLED=true` — enables Content-Security-Policy enforcement via Spatie CSP middleware
- `FPH_ENABLED=true` — enables Permissions-Policy headers

Security headers (HSTS, COOP, `X-Content-Type-Options`, `Referrer-Policy`, `X-Frame-Options`) are applied automatically by `SecurityHeaders` middleware on all web responses.

**Lighthouse note:** Deprecated API warnings for `/cdn-cgi/challenge-platform/scripts/jsd/main.js` come from Cloudflare bot protection injected by Laravel Cloud, not from application code. Run Lighthouse in incognito without browser extensions for accurate scores.

## Cloudinary

Please refer to the respective documentation for the Cloudinary and Cloudinary Nova packages.
Expand Down
21 changes: 18 additions & 3 deletions app/Actions/ViewDataAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,31 @@ 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): array {
])->mapWithKeys(function (string $section) use ($publishedContacts, $locale, $employeeIds): array {
$contacts = $publishedContacts
->filter(function (Contact $contact) use ($section): bool {
->filter(function (Contact $contact) use ($section, $employeeIds): bool {
$sections = $contact->sections ?? [];

return array_key_exists($section, $sections);
if (! array_key_exists($section, $sections)) {
return false;
}

if ($section === ContactSectionEnum::BOARD_MEMBERS && $employeeIds->contains($contact->id)) {
return false;
}

return true;
})
->map(fn (Contact $contact) => ContactDTO::fromModel($contact, $section, $locale));

Expand Down
32 changes: 32 additions & 0 deletions app/Http/Middleware/SecurityHeaders.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class SecurityHeaders
{
/**
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);

if ($request->secure()) {
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
}

$response->headers->set('Cross-Origin-Opener-Policy', 'same-origin');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('X-Frame-Options', 'DENY');

return $response;
}
}
41 changes: 26 additions & 15 deletions app/Security/Presets/MyCspPreset.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,55 @@
use Spatie\Csp\Keyword;
use Spatie\Csp\Policy;
use Spatie\Csp\Preset;
use Spatie\Csp\Value;

class MyCspPreset implements Preset
{
public function configure(Policy $policy): void
{
$policy->add(Directive::BASE, Keyword::SELF);

$policy->add(Directive::DEFAULT, Keyword::SELF);
$cdnHost = parse_url((string) env('AWS_CDN_ENDPOINT', ''), PHP_URL_HOST);

Check failure on line 15 in app/Security/Presets/MyCspPreset.php

View workflow job for this annotation

GitHub Actions / pull-request | ci larastan

Called 'env' outside of the config directory which returns null when the config is cached, use 'config'.

$policy->add(Directive::SCRIPT, [
$scriptSources = array_filter([
Keyword::SELF,
'cdn.usefathom.com',
'cdn-eu.usefathom.com',
$cdnHost ?: null,
]);

$policy->add(Directive::SCRIPT_ELEM, [
Keyword::SELF,
'cdn.usefathom.com',
'cdn-eu.usefathom.com',
]);
$policy->add(Directive::BASE, Keyword::SELF);

$policy->add(Directive::STYLE, [
Keyword::SELF,
Keyword::UNSAFE_INLINE,
]);
$policy->add(Directive::DEFAULT, Keyword::SELF);

$policy->add(Directive::FRAME_ANCESTORS, Keyword::SELF);

$policy->add(Directive::UPGRADE_INSECURE_REQUESTS, Value::NO_VALUE);

$policy->add(Directive::SCRIPT, $scriptSources);

$policy->add(Directive::SCRIPT_ELEM, $scriptSources);

$policy->add(Directive::STYLE_ELEM, [
$styleSources = array_filter([
Keyword::SELF,
Keyword::UNSAFE_INLINE,
$cdnHost ?: null,
]);

$policy->add(Directive::STYLE, $styleSources);

$policy->add(Directive::STYLE_ELEM, $styleSources);

$policy->add(Directive::IMG, [
Keyword::SELF,
'data:',
'res.cloudinary.com',
]);

$policy->add(Directive::FONT, Keyword::SELF);
$fontSources = array_filter([
Keyword::SELF,
$cdnHost ?: null,
]);

$policy->add(Directive::FONT, $fontSources);
$policy->add(Directive::FORM_ACTION, Keyword::SELF);
$policy->add(Directive::MEDIA, Keyword::SELF);
$policy->add(Directive::OBJECT, Keyword::NONE);
Expand Down
58 changes: 58 additions & 0 deletions app/Support/CloudinaryUrl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace App\Support;

class CloudinaryUrl
{
private const CLOUDINARY_HOST = 'res.cloudinary.com';

Check failure on line 7 in app/Support/CloudinaryUrl.php

View workflow job for this annotation

GitHub Actions / pull-request | ci larastan

Out of 9 possible constant types, only 7 - 77.7 % actually have it. Add more constant types to get over 99 %

private const UPLOAD_MARKER = '/image/upload/';

Check failure on line 9 in app/Support/CloudinaryUrl.php

View workflow job for this annotation

GitHub Actions / pull-request | ci larastan

Out of 9 possible constant types, only 7 - 77.7 % actually have it. Add more constant types to get over 99 %

public static function src(string $url, int $width): string
{
return self::transform($url, $width);
}

public static function srcset(string $url, int $width): string
{
$oneX = self::transform($url, $width);
$twoX = self::transform($url, $width * 2);

return "{$oneX} {$width}w, {$twoX} ".($width * 2).'w';
}

public static function transform(string $url, int $width): string
{
if (! str_contains($url, self::CLOUDINARY_HOST)) {
return $url;
}

$markerPos = strpos($url, self::UPLOAD_MARKER);

if ($markerPos === false) {
return $url;
}

$base = substr($url, 0, $markerPos + strlen(self::UPLOAD_MARKER));
$rest = substr($url, $markerPos + strlen(self::UPLOAD_MARKER));
$publicId = self::stripExistingTransforms($rest);
$transforms = "w_{$width},h_{$width},c_fill,f_auto,q_auto";

return $base.$transforms.'/'.$publicId;
}

private static function stripExistingTransforms(string $path): string
{
$segments = explode('/', $path);

if (
isset($segments[0])

Check failure on line 49 in app/Support/CloudinaryUrl.php

View workflow job for this annotation

GitHub Actions / pull-request | ci larastan

Offset 0 on non-empty-list<string> in isset() always exists and is not nullable.
&& preg_match('/^[a-z0-9_,.-]+$/', $segments[0])
&& preg_match('/[whcfq]_/', $segments[0])
) {
array_shift($segments);
}

return implode('/', $segments);
}
}
6 changes: 5 additions & 1 deletion app/View/Components/AppLayout.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@

class AppLayout extends Component
{
public function __construct(protected mixed $page) {}
public function __construct(
protected mixed $page,
public bool $preconnectCloudinary = false,
) {}

public function render(): View
{
Expand All @@ -20,6 +23,7 @@ public function render(): View
'locales' => LocaleEnum::cases(),
'locale' => Str::slug($locale),
'page' => $this->page,
'preconnectCloudinary' => $this->preconnectCloudinary,
'services' => (new ViewDataAction)->services($locale),
'products' => (new ViewDataAction)->products($locale),
]);
Expand Down
2 changes: 2 additions & 0 deletions bootstrap/app.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use App\Http\Middleware\SecurityHeaders;
use App\Http\Middleware\SetLanguage;
use App\Providers\AppServiceProvider;
use App\Providers\EventServiceProvider;
Expand All @@ -25,6 +26,7 @@
$middleware->web(append: [
AddCspHeaders::class,
AddFeaturePolicyHeaders::class,
SecurityHeaders::class,
SetLanguage::class,
CacheResponse::class,
]);
Expand Down
3 changes: 3 additions & 0 deletions config/filesystems.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'visibility' => 'private',
'throw' => false,
'options' => [
'CacheControl' => 'public, max-age=31536000, immutable',
],
],

],
Expand Down
7 changes: 7 additions & 0 deletions config/seo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

return [
'default_image' => 'images/seo/og-codebar.webp',
'image_width' => 1200,
'image_height' => 630,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

return new class extends Migration
{
private const BROKEN_IMAGE_PATTERN = '%seo_codebar.webp%';

private const LEGACY_IMAGE_PATTERN = '%seo_paperflakes.webp%';

/**
* Run the migrations.
*/
public function up(): void
{
DB::table('pages')
->where(function ($query) {
$query->where('image', 'like', self::BROKEN_IMAGE_PATTERN)
->orWhere('image', 'like', self::LEGACY_IMAGE_PATTERN);
})
->update(['image' => null]);

foreach (['news', 'technologies', 'open_sources'] as $table) {
DB::table($table)
->where(function ($query) {
$query->where('image', 'like', self::BROKEN_IMAGE_PATTERN)
->orWhere('image', 'like', self::LEGACY_IMAGE_PATTERN);
})
->update(['image' => '']);
}

$pageDescriptions = [
['key' => 'start.index', 'locale' => 'de_CH', 'description' => 'Wir hören zu, denken konzeptionell und entwickeln nutzerzentrierte Software mit offenen Technologien und Standards.'],
['key' => 'about-us.index', 'locale' => 'de_CH', 'description' => 'Lerne codebar solutions AG kennen – dein Schweizer Partner für konzeptionelle Softwareentwicklung mit offenen Technologien.'],
['key' => 'services.index', 'locale' => 'de_CH', 'description' => 'Wir hören zu, erarbeiten Konzepte für künftige Nutzer:innen und setzen sie um – von der Idee bis zur Software.'],
['key' => 'products.index', 'locale' => 'de_CH', 'description' => 'Nutzerzentrierte Softwarelösungen mit echtem Mehrwert – entwickelt mit offenen Technologien und Standards.'],
['key' => 'contact.index', 'locale' => 'de_CH', 'description' => 'Hast du eine innovative Idee? Wir hören zu, verstehen deine Bedürfnisse und erwecken deine Vision zum Leben.'],
['key' => 'news.index', 'locale' => 'en_CH', 'description' => 'Latest news and expert insights on software development, open technologies and digital innovation from codebar.'],
['key' => 'about-us.index', 'locale' => 'en_CH', 'description' => 'Meet codebar solutions AG – your Swiss partner for conceptual software development with open technologies.'],
];

foreach ($pageDescriptions as $page) {
DB::table('pages')
->where('key', $page['key'])
->where('locale', $page['locale'])
->update(['description' => $page['description']]);
}
}

/**
* Reverse the migrations.
*/
public function down(): void
{
// SEO image and description fixes are not reversed.
}
};
6 changes: 1 addition & 5 deletions database/seeders/Codebar/NewsTableSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,13 @@ public function run(): void
'title' => 'Hello World! codebar stellt sich vor.',
'slug' => 'dhello-world-codebar-stellt-sich-vor',
'teaser' => 'Computerprogramme werden vielfach in fernen Ländern entwickelt, nicht aber bei codebar. Hier gibt es «Software made in Basel». Wir haben Sebastian Fix, den Geschäftsführer dieses Start-ups, nach der Idee dahinter gefragt.',
'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp',
'content' => file_get_contents(database_path('files/news/de_CH/20250406_docuware_712.md')),
'tags' => ['DMS/ECM', 'DocuWare'],
],
'en_CH' => [
'title' => 'DocuWare 7.12 is here',
'slug' => 'docuware-7-12-is-here',
'teaser' => 'More automation, more insights, more efficiency',
'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp',
'content' => file_get_contents(database_path('files/news/en_CH/20250406_docuware_712.md')),
'tags' => ['DMS/ECM', 'DocuWare'],

Expand All @@ -48,15 +46,13 @@ public function run(): void
'title' => 'DocuWare und codebar Solutions AG: Zwei Partner, eine Mission',
'slug' => 'docu-ware-cloud-partner',
'teaser' => 'Die codebar Solutions AG ist seit Februar 2023 offizieller Partner der Dokumenten-Management-Lösung (DMS) DocuWare Cloud. Dadurch haben unsere Kund*innen ab sofort ein Tool an der Hand, welches ihnen helfen wird, die Digitalisierung im eigenen Unternehmen voranzutreiben.',
'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp',
'content' => file_get_contents(database_path('files/news/de_CH/20250406_docuware_712.md')),
'tags' => ['DMS/ECM', 'DocuWare'],
],
'en_CH' => [
'title' => 'DocuWare 7.12 is here',
'slug' => 'docuware-7-12-is-here',
'teaser' => 'More automation, more insights, more efficiency',
'image' => 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-paperflakes-ch/seo/seo_paperflakes.webp',
'content' => file_get_contents(database_path('files/news/en_CH/20250406_docuware_712.md')),
'tags' => ['DMS/ECM', 'DocuWare'],

Expand All @@ -81,7 +77,7 @@ private function seed(Carbon $publishedAt, string $author, array $localizedData)
'published_at' => $publishedAt,
'title' => Arr::get($data, 'title'),
'teaser' => Arr::get($data, 'teaser'),
'image' => Arr::get($data, 'image'),
'image' => Arr::get($data, 'image', ''),
'tags' => Arr::get($data, 'tags', []),
'content' => Arr::get($data, 'content'),
]
Expand Down
Loading
Loading