diff --git a/app/Http/Controllers/Zunscan/AboutController.php b/app/Http/Controllers/Zunscan/AboutController.php
new file mode 100644
index 0000000..280fe09
--- /dev/null
+++ b/app/Http/Controllers/Zunscan/AboutController.php
@@ -0,0 +1,19 @@
+ __('zunscan.seo.about.title'),
+ 'description' => __('zunscan.seo.about.description'),
+ 'image' => self::OG_IMAGE,
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Zunscan/ContactController.php b/app/Http/Controllers/Zunscan/ContactController.php
new file mode 100644
index 0000000..e70324c
--- /dev/null
+++ b/app/Http/Controllers/Zunscan/ContactController.php
@@ -0,0 +1,19 @@
+ __('zunscan.seo.contact.title'),
+ 'description' => __('zunscan.seo.contact.description'),
+ 'image' => self::OG_IMAGE,
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Zunscan/MediaController.php b/app/Http/Controllers/Zunscan/MediaController.php
new file mode 100644
index 0000000..73148ef
--- /dev/null
+++ b/app/Http/Controllers/Zunscan/MediaController.php
@@ -0,0 +1,19 @@
+ __('zunscan.seo.media.title'),
+ 'description' => __('zunscan.seo.media.description'),
+ 'image' => self::OG_IMAGE,
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Zunscan/PrivacyController.php b/app/Http/Controllers/Zunscan/PrivacyController.php
new file mode 100644
index 0000000..8c3fa42
--- /dev/null
+++ b/app/Http/Controllers/Zunscan/PrivacyController.php
@@ -0,0 +1,22 @@
+ __('zunscan.seo.privacy.title'),
+ 'description' => __('zunscan.seo.privacy.description'),
+ 'image' => self::OG_IMAGE,
+ 'body' => Str::markdown(File::get(resource_path('content/zunscan/'.app()->getLocale().'/datenschutz.md'))),
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Zunscan/RobotsController.php b/app/Http/Controllers/Zunscan/RobotsController.php
new file mode 100644
index 0000000..246da25
--- /dev/null
+++ b/app/Http/Controllers/Zunscan/RobotsController.php
@@ -0,0 +1,41 @@
+getSchemeAndHttpHost().'/sitemap.xml';
+
+ return response(content: $this->content($sitemapUrl), headers: [
+ 'Content-Type' => 'text/plain',
+ ]);
+ }
+
+ private function content(string $sitemapUrl): string
+ {
+ if (! app()->isProduction()) {
+ return implode("\n", [
+ 'User-agent: *',
+ 'Disallow: /',
+ ]);
+ }
+
+ return implode("\n", [
+ 'User-agent: *',
+ 'Disallow:',
+ '',
+ 'Sitemap: '.$sitemapUrl,
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Zunscan/ServicesScanningController.php b/app/Http/Controllers/Zunscan/ServicesScanningController.php
new file mode 100644
index 0000000..123ec33
--- /dev/null
+++ b/app/Http/Controllers/Zunscan/ServicesScanningController.php
@@ -0,0 +1,19 @@
+ __('zunscan.seo.scanning.title'),
+ 'description' => __('zunscan.seo.scanning.description'),
+ 'image' => self::OG_IMAGE,
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Zunscan/SitemapController.php b/app/Http/Controllers/Zunscan/SitemapController.php
new file mode 100644
index 0000000..7c7a897
--- /dev/null
+++ b/app/Http/Controllers/Zunscan/SitemapController.php
@@ -0,0 +1,46 @@
+getSchemeAndHttpHost();
+
+ $urls = collect(self::LOCALES)
+ ->crossJoin(self::PAGES)
+ ->map(fn (array $pair): string => sprintf(
+ '%s ',
+ e($host.route('zunscan.'.$pair[0].'.'.$pair[1], absolute: false))
+ ))
+ ->implode('');
+
+ $xml = ''
+ .''
+ .$urls
+ .' ';
+
+ return response(content: $xml)
+ ->header('Content-Type', 'application/xml')
+ ->header('Cache-Control', 'public, max-age=3600');
+ }
+}
diff --git a/app/Http/Controllers/Zunscan/StartController.php b/app/Http/Controllers/Zunscan/StartController.php
new file mode 100644
index 0000000..030c58f
--- /dev/null
+++ b/app/Http/Controllers/Zunscan/StartController.php
@@ -0,0 +1,19 @@
+ __('zunscan.seo.start.title'),
+ 'description' => __('zunscan.seo.start.description'),
+ 'image' => self::OG_IMAGE,
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Zunscan/TermsController.php b/app/Http/Controllers/Zunscan/TermsController.php
new file mode 100644
index 0000000..2a56b38
--- /dev/null
+++ b/app/Http/Controllers/Zunscan/TermsController.php
@@ -0,0 +1,22 @@
+ __('zunscan.seo.imprint.title'),
+ 'description' => __('zunscan.seo.imprint.description'),
+ 'image' => self::OG_IMAGE,
+ 'body' => Str::markdown(File::get(resource_path('content/zunscan/'.app()->getLocale().'/impressum.md'))),
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Zunscan/ZunscanController.php b/app/Http/Controllers/Zunscan/ZunscanController.php
new file mode 100644
index 0000000..f950f26
--- /dev/null
+++ b/app/Http/Controllers/Zunscan/ZunscanController.php
@@ -0,0 +1,19 @@
+route()?->getName();
+
+ foreach (LocaleEnum::cases() as $locale) {
+ if (Str::startsWith($routeName, 'zunscan.'.Str::slug($locale->value).'.')) {
+ app()->setLocale($locale->value);
+ break;
+ }
+ }
+
+ URL::forceRootUrl($request->getSchemeAndHttpHost());
+
+ return $next($request);
+ }
+}
diff --git a/app/Support/helpers.php b/app/Support/helpers.php
index c065175..3167332 100644
--- a/app/Support/helpers.php
+++ b/app/Support/helpers.php
@@ -54,3 +54,53 @@ function locale_switch_url(string $locale): string
}
}
}
+
+if (! function_exists('zunscan_route')) {
+ /**
+ * Zunscan's own version of localized_route(): its route names are
+ * "zunscan.{locale}.{rest}" rather than "{locale}.{rest}", so the
+ * locale slug has to be inserted after the "zunscan." prefix instead
+ * of standing in front of it.
+ *
+ * Defaults to a relative path — deliberately, not just for convenience.
+ * AppServiceProvider forces every *absolute* route()/url() call app-wide
+ * to config('app.url') (www.codebar.ch), because that is correct for the
+ * main site. Zunscan lives on a different host, so an absolute call here
+ * would silently point at the wrong domain; a relative path sidesteps
+ * that forced root entirely and still resolves correctly in the browser.
+ */
+ function zunscan_route(string $name, mixed $parameters = [], bool $absolute = false): string
+ {
+ $locale = Str::slug(app()->getLocale());
+
+ return route('zunscan.'.$locale.'.'.$name, $parameters, $absolute);
+ }
+}
+
+if (! function_exists('zunscan_locale_switch_url')) {
+ /**
+ * The current Zunscan page's URL in another locale, as a real absolute URL
+ * (hreflang tags and the switcher both need one). Built from the request's
+ * own host rather than route()'s absolute mode, for the same reason as
+ * zunscan_route() above.
+ */
+ function zunscan_locale_switch_url(string $locale): string
+ {
+ $localeSlug = Str::slug($locale);
+ $routeName = (string) request()->route()?->getName();
+ $prefix = 'zunscan.'.Str::slug(app()->getLocale()).'.';
+ $host = request()->getSchemeAndHttpHost();
+
+ if (! Str::startsWith($routeName, $prefix)) {
+ return $host.route('zunscan.'.$localeSlug.'.start.index', absolute: false);
+ }
+
+ $routeKey = Str::after($routeName, $prefix);
+
+ try {
+ return $host.route('zunscan.'.$localeSlug.'.'.$routeKey, absolute: false);
+ } catch (Throwable) {
+ return $host.route('zunscan.'.$localeSlug.'.start.index', absolute: false);
+ }
+ }
+}
diff --git a/composer.lock b/composer.lock
index 32bbc05..3142c9f 100644
--- a/composer.lock
+++ b/composer.lock
@@ -62,16 +62,16 @@
},
{
"name": "aws/aws-sdk-php",
- "version": "3.390.2",
+ "version": "3.390.5",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "a09e95bf062e15b7343f9c1362fd45c7a0dec39b"
+ "reference": "c56ba4aafe1e8e6d80610c4e6506bbd655994bd7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a09e95bf062e15b7343f9c1362fd45c7a0dec39b",
- "reference": "a09e95bf062e15b7343f9c1362fd45c7a0dec39b",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c56ba4aafe1e8e6d80610c4e6506bbd655994bd7",
+ "reference": "c56ba4aafe1e8e6d80610c4e6506bbd655994bd7",
"shasum": ""
},
"require": {
@@ -153,9 +153,9 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
- "source": "https://github.com/aws/aws-sdk-php/tree/3.390.2"
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.390.5"
},
- "time": "2026-07-31T18:06:07+00:00"
+ "time": "2026-08-05T18:11:23+00:00"
},
{
"name": "brick/math",
@@ -1069,21 +1069,21 @@
},
{
"name": "guzzlehttp/guzzle",
- "version": "7.15.2",
+ "version": "7.15.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "744101956d78b7c1384d0cbf379db13e859167bf"
+ "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/744101956d78b7c1384d0cbf379db13e859167bf",
- "reference": "744101956d78b7c1384d0cbf379db13e859167bf",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc",
+ "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc",
"shasum": ""
},
"require": {
"ext-json": "*",
- "guzzlehttp/promises": "^2.5.1",
+ "guzzlehttp/promises": "^2.5.2",
"guzzlehttp/psr7": "^2.13",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
@@ -1177,7 +1177,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
- "source": "https://github.com/guzzle/guzzle/tree/7.15.2"
+ "source": "https://github.com/guzzle/guzzle/tree/7.15.3"
},
"funding": [
{
@@ -1193,20 +1193,20 @@
"type": "tidelift"
}
],
- "time": "2026-07-26T23:23:20+00:00"
+ "time": "2026-08-05T19:48:21+00:00"
},
{
"name": "guzzlehttp/promises",
- "version": "2.5.1",
+ "version": "2.5.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
- "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29"
+ "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/promises/zipball/9ad1e4fc607446a055b95870c7f668e93b5cff29",
- "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/2823687acff28b2dbe67b2508a6b300e2c3fa4ce",
+ "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce",
"shasum": ""
},
"require": {
@@ -1261,7 +1261,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
- "source": "https://github.com/guzzle/promises/tree/2.5.1"
+ "source": "https://github.com/guzzle/promises/tree/2.5.2"
},
"funding": [
{
@@ -1277,7 +1277,7 @@
"type": "tidelift"
}
],
- "time": "2026-07-08T15:48:39+00:00"
+ "time": "2026-08-05T19:30:54+00:00"
},
{
"name": "guzzlehttp/psr7",
@@ -1486,16 +1486,16 @@
},
{
"name": "laravel/framework",
- "version": "v13.23.0",
+ "version": "v13.24.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "92a707229148e57f08a249211c8a5a194159c619"
+ "reference": "6d481710375d2aa67656922ef760cdd2b18bcfe0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/92a707229148e57f08a249211c8a5a194159c619",
- "reference": "92a707229148e57f08a249211c8a5a194159c619",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/6d481710375d2aa67656922ef760cdd2b18bcfe0",
+ "reference": "6d481710375d2aa67656922ef760cdd2b18bcfe0",
"shasum": ""
},
"require": {
@@ -1515,7 +1515,7 @@
"guzzlehttp/guzzle": "^7.8.2",
"guzzlehttp/promises": "^2.0.3",
"guzzlehttp/uri-template": "^1.0",
- "laravel/prompts": "^0.3.0",
+ "laravel/prompts": "^0.3.11",
"laravel/serializable-closure": "^2.0.10",
"league/commonmark": "^2.8.1",
"league/flysystem": "^3.25.1",
@@ -1709,7 +1709,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2026-07-27T14:48:58+00:00"
+ "time": "2026-08-04T15:54:59+00:00"
},
{
"name": "laravel/nightwatch",
@@ -1807,16 +1807,16 @@
},
{
"name": "laravel/prompts",
- "version": "v0.3.21",
+ "version": "v0.3.22",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
- "reference": "7753c65c281c2550c7c183f14e18062073b7d821"
+ "reference": "02b89b39e8972a998db4d5d4ad4719239dd4aee4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/prompts/zipball/7753c65c281c2550c7c183f14e18062073b7d821",
- "reference": "7753c65c281c2550c7c183f14e18062073b7d821",
+ "url": "https://api.github.com/repos/laravel/prompts/zipball/02b89b39e8972a998db4d5d4ad4719239dd4aee4",
+ "reference": "02b89b39e8972a998db4d5d4ad4719239dd4aee4",
"shasum": ""
},
"require": {
@@ -1860,9 +1860,9 @@
"description": "Add beautiful and user-friendly forms to your command-line applications.",
"support": {
"issues": "https://github.com/laravel/prompts/issues",
- "source": "https://github.com/laravel/prompts/tree/v0.3.21"
+ "source": "https://github.com/laravel/prompts/tree/v0.3.22"
},
- "time": "2026-06-26T00:11:25+00:00"
+ "time": "2026-08-04T14:50:50+00:00"
},
{
"name": "laravel/serializable-closure",
@@ -1996,16 +1996,16 @@
},
{
"name": "league/commonmark",
- "version": "2.8.3",
+ "version": "2.9.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
- "reference": "1902f60f984235023acbe03db6ad614a37b3c3e7"
+ "reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/1902f60f984235023acbe03db6ad614a37b3c3e7",
- "reference": "1902f60f984235023acbe03db6ad614a37b3c3e7",
+ "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/5703d83ba3da3b2e356a5fedc848ed6d8ffb6529",
+ "reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529",
"shasum": ""
},
"require": {
@@ -2042,7 +2042,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "2.9-dev"
+ "dev-main": "2.10-dev"
}
},
"autoload": {
@@ -2099,7 +2099,7 @@
"type": "tidelift"
}
],
- "time": "2026-07-12T15:29:16+00:00"
+ "time": "2026-08-03T13:42:31+00:00"
},
{
"name": "league/config",
@@ -9144,16 +9144,16 @@
},
{
"name": "laravel/pint",
- "version": "v1.30.2",
+ "version": "v1.30.4",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
- "reference": "de440809cb86055b43e7ae979bed3baf8d29ef5a"
+ "reference": "a96cb6eee2961905d2fce7207aefb80945bf6b28"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pint/zipball/de440809cb86055b43e7ae979bed3baf8d29ef5a",
- "reference": "de440809cb86055b43e7ae979bed3baf8d29ef5a",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/a96cb6eee2961905d2fce7207aefb80945bf6b28",
+ "reference": "a96cb6eee2961905d2fce7207aefb80945bf6b28",
"shasum": ""
},
"require": {
@@ -9166,11 +9166,11 @@
"require-dev": {
"composer/semver": "^3.4.4",
"friendsofphp/php-cs-fixer": "^3.95.18",
- "illuminate/view": "^12.64.0",
+ "illuminate/view": "^12.65.0",
"larastan/larastan": "^3.10.0",
"laravel-zero/framework": "^12.1.0",
"laravel/agent-detector": "^2.0.2",
- "laravel/prompts": "^0.3.21",
+ "laravel/prompts": "^0.3.22",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.4.0",
"pestphp/pest": "^3.8.7"
@@ -9210,20 +9210,20 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
- "time": "2026-08-01T01:14:35+00:00"
+ "time": "2026-08-05T16:47:22+00:00"
},
{
"name": "laravel/sail",
- "version": "v1.64.0",
+ "version": "v1.65.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sail.git",
- "reference": "08cacd3e72d6798df3fa8bd4b5d55d0f7f920625"
+ "reference": "d4b92139858d2a189a6302ca133e494f58afe20b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/sail/zipball/08cacd3e72d6798df3fa8bd4b5d55d0f7f920625",
- "reference": "08cacd3e72d6798df3fa8bd4b5d55d0f7f920625",
+ "url": "https://api.github.com/repos/laravel/sail/zipball/d4b92139858d2a189a6302ca133e494f58afe20b",
+ "reference": "d4b92139858d2a189a6302ca133e494f58afe20b",
"shasum": ""
},
"require": {
@@ -9273,7 +9273,7 @@
"issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail"
},
- "time": "2026-07-17T15:09:35+00:00"
+ "time": "2026-08-03T18:00:17+00:00"
},
{
"name": "mockery/mockery",
@@ -9595,16 +9595,16 @@
},
{
"name": "pestphp/pest",
- "version": "v4.7.7",
+ "version": "v4.7.8",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest.git",
- "reference": "7d187dd50a92bcc52caacc8af32a1b9f3f87dd95"
+ "reference": "5b2293f67adcf1b2320b33f521b94a692d18f360"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pestphp/pest/zipball/7d187dd50a92bcc52caacc8af32a1b9f3f87dd95",
- "reference": "7d187dd50a92bcc52caacc8af32a1b9f3f87dd95",
+ "url": "https://api.github.com/repos/pestphp/pest/zipball/5b2293f67adcf1b2320b33f521b94a692d18f360",
+ "reference": "5b2293f67adcf1b2320b33f521b94a692d18f360",
"shasum": ""
},
"require": {
@@ -9697,7 +9697,7 @@
],
"support": {
"issues": "https://github.com/pestphp/pest/issues",
- "source": "https://github.com/pestphp/pest/tree/v4.7.7"
+ "source": "https://github.com/pestphp/pest/tree/v4.7.8"
},
"funding": [
{
@@ -9709,7 +9709,7 @@
"type": "github"
}
],
- "time": "2026-08-01T01:52:59+00:00"
+ "time": "2026-08-03T20:49:44+00:00"
},
{
"name": "pestphp/pest-plugin",
@@ -10710,11 +10710,11 @@
},
{
"name": "phpstan/phpstan",
- "version": "2.2.7",
+ "version": "2.2.8",
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/692db47b9dddb0487934e5236e77d48594aef921",
- "reference": "692db47b9dddb0487934e5236e77d48594aef921",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e285254e60f33c21902efef4a926ca0987c06804",
+ "reference": "e285254e60f33c21902efef4a926ca0987c06804",
"shasum": ""
},
"require": {
@@ -10770,7 +10770,7 @@
"type": "github"
}
],
- "time": "2026-07-29T17:39:32+00:00"
+ "time": "2026-08-04T22:21:45+00:00"
},
{
"name": "phpunit/php-code-coverage",
diff --git a/config/zunscan.php b/config/zunscan.php
new file mode 100644
index 0000000..c71c504
--- /dev/null
+++ b/config/zunscan.php
@@ -0,0 +1,70 @@
+ env('ZUNSCAN_DOMAIN', 'zunscan.codebar.ch'),
+
+ // zunscan.ch is run by paperflakes AG, a joint venture of these two
+ // companies — one contact person each. Config rather than the database:
+ // this sub-site deliberately has no models, and none of these fields are
+ // translated (the company name doubles as the role). Portraits are
+ // Cloudinary URLs, which is the only remote image host the CSP allows.
+ 'people' => [
+ [
+ 'key' => 'real-estate-club',
+ 'name' => 'Mischa Lanz',
+ 'company' => 'Real Estate Club GmbH',
+ 'email' => 'mischa.lanz@realestateclub.ch',
+ 'phone' => '+41 61 515 20 40',
+ 'website' => 'https://www.realestateclub.ch/',
+ 'website_label' => 'www.realestateclub.ch',
+ 'linkedin' => 'https://www.linkedin.com/in/mischa-lanz-672a65112/',
+ 'image' => 'https://res.cloudinary.com/codebar/image/upload/w_400,h_400,f_auto,q_auto/www-paperflakes-ch/people/6528f7c6bddf8430fb5d154c_Mischa_Hemd.webp',
+ ],
+ [
+ 'key' => 'codebar',
+ 'name' => 'Sebastian Bürgin-Fix',
+ 'company' => 'codebar Solutions AG',
+ 'email' => 'sebastian.buergin@codebar.ch',
+ 'phone' => '+41 61 515 60 95',
+ 'website' => 'https://www.codebar.ch/',
+ 'website_label' => 'www.codebar.ch',
+ 'linkedin' => 'https://www.linkedin.com/in/sebastian-buergin/',
+ 'image' => 'https://res.cloudinary.com/codebar/image/upload/e_background_removal/c_thumb,g_face,z_0.7,w_500,h_500,b_rgb:E4E6E9/f_auto,q_auto/www-codebar-ch/team/Sebastian_V3.webp',
+ ],
+ ],
+
+ // One entry per address, not per company: codebar has two sites, and
+ // stacking them inside a single card left the Real Estate Club card
+ // half empty next to it.
+ 'locations' => [
+ [
+ 'company' => 'Real Estate Club GmbH',
+ 'street' => 'Hauptstrasse 91',
+ 'city' => 'CH-4455 Zunzgen',
+ ],
+ [
+ 'company' => 'codebar Solutions AG',
+ 'street' => 'Langegasse 39',
+ 'city' => 'CH-4104 Oberwil',
+ ],
+ ],
+
+ // paperflakes AG's registered address — the operating entity behind
+ // zunscan.ch. Single source for the imprint and the JSON-LD graph.
+ 'company' => [
+ 'legal_name' => 'paperflakes AG',
+ 'street' => 'Hauptstrasse 91',
+ 'postal_code' => '4455',
+ 'locality' => 'Zunzgen',
+ 'uid' => 'CHE-432.585.498',
+ 'email' => 'info@paperflakes.ch',
+ ],
+
+];
diff --git a/lang/de_CH/zunscan.php b/lang/de_CH/zunscan.php
new file mode 100644
index 0000000..8955ebe
--- /dev/null
+++ b/lang/de_CH/zunscan.php
@@ -0,0 +1,178 @@
+ [
+ 'start' => 'Start',
+ 'scanning' => 'Digitalisierung',
+ 'about' => 'Über uns',
+ 'contact' => 'Kontakt',
+ 'home' => 'Home',
+ 'media' => 'Medien',
+ 'imprint' => 'Impressum',
+ 'privacy' => 'Datenschutz',
+ 'open_menu' => 'Hauptmenü öffnen',
+ 'close_menu' => 'Menü schliessen',
+ ],
+
+ 'seo' => [
+ 'start' => [
+ 'title' => 'Herzlich Willkommen',
+ 'description' => 'zunscan.ch - Dein Scanningcenter: Platzersparnis, Post-Scanning, Volltext-Suche. Starte jetzt in die Zukunft mit maximaler Effizienz!',
+ ],
+ 'about' => [
+ 'title' => 'Über uns',
+ 'description' => 'Starte am Büchel mit uns in die Zukunft der Dokumentenverwaltung. Platzersparnis, Post-Scanning und Volltextsuche. Lokal, regional aus 4455 Zunzgen',
+ ],
+ 'scanning' => [
+ 'title' => 'Digitalisierung',
+ 'description' => 'Scanndienste für Top-Preise: Archivieren von A4 und A3 Seiten, Pläne, Bücher, Bundesordnern & mehr. Zunscan.ch - Dein Partner für digitale Lösungen!',
+ ],
+ 'contact' => [
+ 'title' => 'Kontakt',
+ 'description' => 'Zwei Ansprechpersonen, ein Ansprechpartner: zunscan.ch wird von der paperflakes AG betrieben, einem Gemeinschaftsunternehmen von codebar Solutions AG und Real Estate Club GmbH.',
+ ],
+ 'media' => [
+ 'title' => 'Medien',
+ 'description' => 'Entdecke unsere Medienrubrik: Logos, Presseberichte und Neuigkeiten rund um Scannen, Dokumentenverwaltung und OCR. Bleibe informiert bei Zunscan.ch!',
+ ],
+ 'imprint' => [
+ 'title' => 'Impressum',
+ 'description' => 'Keine Gewähr für Inhaltsgenauigkeit, keine Haftung für Links. Urheberrechte: Alle Rechte bei paperflakes AG. Schriftliche Zustimmung für Reproduktion.',
+ ],
+ 'privacy' => [
+ 'title' => 'Datenschutz',
+ 'description' => 'Datenschutz gemäss DSGVO: Wir behandeln Ihre Daten vertraulich, nutzen sie nur mit Einwilligung. SSL-Verschlüsselung schützt Ihre Übertragungen. Widerruf möglich.',
+ ],
+ ],
+
+ 'start' => [
+ 'hero_title' => 'Das Scanning Center in der Nordwestschweiz',
+ 'subtitle' => 'Wir schaffen Platz und Ordnung in deinem Büro',
+ 'solutions_eyebrow' => 'Unsere Leistungen',
+ 'solutions_title' => 'Wir bieten dir folgende Lösungen an',
+ 'space_title' => 'Platzersparnis',
+ 'space_body' => 'Wir scannen deine Dokumente, du wirst die Ordner los. Der Platz, den heute Regale und Archivräume belegen, steht dir wieder zum Arbeiten zur Verfügung.',
+ // mail_*: aktuell nicht angezeigt (siehe zunscan/start/index.blade.php),
+ // gehoert zur deaktivierten ePost-Dienstleistung. Text bleibt gepflegt.
+ 'mail_title' => 'Post-Scanning-Dienst',
+ 'mail_body' => 'Wir digitalisieren eingehende physische Post und stellen sie dir digital zur Verfügung. Dadurch erhöhen wir nicht nur deine Effizienz, sondern ermöglichen auch einen nahtlosen Übergang von physischen zu digitalen Arbeitsabläufen.',
+ 'compliance_title' => 'Digitalisierung und Revisionssicherheit',
+ 'compliance_body' => 'Alle Mitarbeitenden greifen jederzeit und von überall sicher zu. Wir archivieren revisionssicher, gesetzeskonform und lückenlos nachvollziehbar.',
+ 'ocr_title' => 'Volltext-Suche in Sekunden',
+ 'ocr_body' => 'Unsere Volltexterkennung (OCR) macht jede Seite durchsuchbar. Was vorher Blättern im Archiv war, ist danach eine Suche von wenigen Sekunden.',
+ 'dms_title' => 'DMS & ECM einrichten',
+ 'dms_body' => 'Wir hören nicht beim Scannen auf: Auf Wunsch richten wir dein Dokumentenmanagement (DMS/ECM) ein, damit die Dateien in einem System landen, das mitwächst.',
+ 'pricing_title' => 'Was kostet die Digitalisierung deines Büros?',
+ 'pricing_body' => 'Schau dir unsere Preisliste an.',
+ ],
+
+ 'about' => [
+ 'title' => 'Über uns',
+ 'subtitle' => 'Ein Scanning Center am Büchel, keine anonyme Grossfirma',
+ 'work_eyebrow' => 'Was wir tun',
+ 'what_we_do_title' => 'Papier rein, durchsuchbare Dokumente raus',
+ 'lead' => 'Wir digitalisieren Papier – vom einzelnen Bundesordner bis zum gewachsenen Archiv. Alles passiert bei uns in Zunzgen: Deine Unterlagen verlassen die Nordwestschweiz nicht.',
+ 'what_title' => 'Aufbereiten und scannen',
+ 'what_body' => 'Wir bereiten deine Dokumente von Hand auf, entfernen Klammern, Register und Hüllen, scannen sie und legen eine Volltexterkennung darüber. Zurück bekommst du Dateien, in denen du in Sekunden findest, wofür du vorher durch Ordner geblättert hast.',
+ 'how_title' => 'Nachvollziehbar arbeiten',
+ 'how_body' => 'Wir dokumentieren jeden Schritt, damit die Digitalisierung revisionssicher und gesetzeskonform bleibt. Auf Wunsch vernichten wir die Originale danach zertifiziert – oder du bekommst sie unverändert zurück.',
+ 'who_eyebrow' => 'Wer hinter zunscan.ch steht',
+ 'who_title' => 'Zwei Unternehmen aus der Region',
+ 'who_lead' => 'zunscan.ch wird von der paperflakes AG betrieben. Dahinter stehen zwei Firmen, die sich ergänzen – und bei beiden sprichst du direkt mit einer Ansprechperson.',
+ 'partners' => [
+ 'real-estate-club' => 'Sorgt für alles vor Ort in Zunzgen: Räumlichkeiten, Logistik und die kurzen Wege zu Kundinnen und Kunden aus der Region.',
+ 'codebar' => 'Sorgt für die Technik dahinter: Volltexterkennung, saubere Prozesse und ein Dokumentenmanagement, das im Alltag trägt.',
+ ],
+ ],
+
+ 'contact' => [
+ 'title' => 'Kontakt',
+ 'subtitle' => 'Gerne für dich da, digital und analog',
+ 'eyebrow' => 'Zwei Firmen, zwei Ansprechpersonen',
+ 'heading' => 'Ein Gemeinschaftsunternehmen mit kurzen Wegen',
+ 'body' => 'zunscan.ch wird von der paperflakes AG betrieben – einem Gemeinschaftsunternehmen der codebar Solutions AG und der Real Estate Club GmbH. Du erreichst bei beiden Partnerinnen direkt eine Ansprechperson, ohne Umweg über ein Formular.',
+ 'locations_title' => 'Standorte',
+ 'write_to' => 'E-Mail an :name schreiben',
+ 'call' => ':name anrufen',
+ 'linkedin_of' => 'LinkedIn-Profil von :name',
+ ],
+
+ 'media' => [
+ 'title' => 'Medien',
+ 'subtitle' => 'Logos, Neuigkeiten und Berichte rund um zunscan.ch',
+ 'logo_alt' => 'Logo zunscan.ch',
+ 'download' => 'Logo herunterladen',
+ ],
+
+ 'legal' => [
+ 'imprint_title' => 'Impressum',
+ 'imprint_subtitle' => 'Wer diese Website betreibt',
+ 'privacy_title' => 'Datenschutz',
+ 'privacy_subtitle' => 'Was mit deinen Daten passiert',
+ 'company_title' => 'Firma',
+ 'uid' => 'UID',
+ 'email' => 'E-Mail',
+ ],
+
+ 'common' => [
+ 'price' => 'Preis',
+ 'disposal' => 'Entsorgung',
+ 'service' => 'Service',
+ 'preparation' => 'Arbeitsvorbereitung',
+ 'remove_staples' => 'Entfernen von Heftklammern',
+ 'remove_paperclips' => 'Entfernen von Büroklammern',
+ 'remove_tabs' => 'Entfernen von Register',
+ 'remove_sleeves' => 'Entfernen von Klarsichthüllen',
+ 'digitization_ocr' => 'Digitalisierung inkl. OCR',
+ ],
+
+ 'services' => [
+ 'scanning' => [
+ 'title' => 'Digitalisierung',
+ 'subtitle' => 'Wir verwandeln mit hochwertigem Equipment Papier zu Pixel und Ordner zu Bytes',
+ 'prices_eyebrow' => 'Transparent kalkuliert',
+ 'prices_title' => 'Preise',
+ 'folder_a4_title' => 'Preis pro Bundesordner A4',
+ 'folder_a4_pages' => 'Bis 430 Seiten',
+ 'folder_a4_price' => 'CHF 64.50',
+ 'folder_a4_disposal' => 'CHF 1.50 / kg',
+ 'page_a3a4_title' => 'Preis pro Seite A3/A4',
+ 'page_a3a4_price' => 'CHF 0.19',
+ 'page_a3a4_disposal' => 'CHF 1.50 / kg',
+ 'flat_fee_title' => 'Auftragspauschale',
+ 'flat_fee_price' => 'Für den ersten Auftrag CHF 499.00',
+ 'more_services_title' => 'Weitere Dienstleistungen',
+ 'disposal_title' => 'Entsorgung',
+ 'disposal_subtitle' => 'Du erhältst für die Vernichtung ein Zertifikat der Reisswolf Aktenvernichtung AG.',
+ 'disposal_price_label' => 'Pro Vernichtungsauftrag',
+ 'disposal_price' => 'CHF 250.00',
+ 'return_title' => 'Rückführung',
+ 'return_subtitle' => 'Du erhältst deine Akten zurück im Übermittlungszustand.',
+ 'return_folder_label' => 'Ordner A4',
+ 'return_folder_price' => 'CHF 108.50',
+ 'return_page_label' => 'Seite A3/A4',
+ 'return_page_price' => 'CHF 0.31',
+ 'other_title' => 'Sonstiges',
+ 'other_subtitle' => 'Du hast ein anderes Format, das du digitalisieren möchtest?',
+ 'other_price_label' => 'Nach Aufwand',
+ 'other_price' => 'CHF 90.00 / h',
+ ],
+ ],
+
+ 'cta' => [
+ 'heading' => 'Kontaktiere uns für ein massgeschneidertes Angebot',
+ 'button' => 'Zum Kontakt',
+ ],
+
+ 'footer' => [
+ 'heading' => 'Footer',
+ 'nav_title' => 'Navigation',
+ 'legal_title' => 'Rechtliches',
+ 'copyright' => '© :year Zunscan. Ein Projekt der :companies.',
+ 'swiss_digital_services' => 'swiss digital services',
+ ],
+
+];
diff --git a/lang/en_CH/zunscan.php b/lang/en_CH/zunscan.php
new file mode 100644
index 0000000..8e04562
--- /dev/null
+++ b/lang/en_CH/zunscan.php
@@ -0,0 +1,178 @@
+ [
+ 'start' => 'Home',
+ 'scanning' => 'Digitization',
+ 'about' => 'About us',
+ 'contact' => 'Contact',
+ 'home' => 'Home',
+ 'media' => 'Media',
+ 'imprint' => 'Imprint',
+ 'privacy' => 'Privacy',
+ 'open_menu' => 'Open main menu',
+ 'close_menu' => 'Close menu',
+ ],
+
+ 'seo' => [
+ 'start' => [
+ 'title' => 'Welcome',
+ 'description' => 'zunscan.ch - your scanning centre: free up office space, digitize incoming mail, search full text in seconds. Start into a more efficient future today!',
+ ],
+ 'about' => [
+ 'title' => 'About us',
+ 'description' => 'Start your journey into the future of document management with us. Free up space, digitize incoming mail, and search full text. Local, from 4455 Zunzgen.',
+ ],
+ 'scanning' => [
+ 'title' => 'Digitization',
+ 'description' => 'Document scanning at unbeatable prices: A4 and A3 pages, plans, books, ring binders and more. zunscan.ch - your partner for digital solutions!',
+ ],
+ 'contact' => [
+ 'title' => 'Contact',
+ 'description' => 'Two contacts, one partner: zunscan.ch is run by paperflakes AG, a joint venture of codebar Solutions AG and Real Estate Club GmbH.',
+ ],
+ 'media' => [
+ 'title' => 'Media',
+ 'description' => 'Logos, press coverage and news about scanning, document management and OCR. Stay up to date with zunscan.ch!',
+ ],
+ 'imprint' => [
+ 'title' => 'Imprint',
+ 'description' => 'No warranty for the accuracy of content, no liability for links. Copyright: all rights reserved by paperflakes AG. Written consent required for reproduction.',
+ ],
+ 'privacy' => [
+ 'title' => 'Privacy',
+ 'description' => 'Privacy in line with the GDPR: we treat your data confidentially and only process it with your consent. SSL encryption protects your data in transit. Consent can be withdrawn at any time.',
+ ],
+ ],
+
+ 'start' => [
+ 'hero_title' => 'The scanning centre in north-western Switzerland',
+ 'subtitle' => 'We create space and order in your office',
+ 'solutions_eyebrow' => 'What we offer',
+ 'solutions_title' => 'Here is what we can do for you',
+ 'space_title' => 'Free up space',
+ 'space_body' => 'We scan your documents and you lose the folders. The space that shelves and archive rooms take up today goes back to being room to work in.',
+ // mail_*: currently not rendered (see zunscan/start/index.blade.php),
+ // belongs to the disabled ePost service. Copy is kept in sync.
+ 'mail_title' => 'Post-scanning service',
+ 'mail_body' => 'We digitize your incoming physical mail and make it available to you online. That does not just boost your efficiency — it also gives you a seamless move from paper to digital workflows.',
+ 'compliance_title' => 'Digitization with audit-proof compliance',
+ 'compliance_body' => 'Every employee has secure access, anytime and from anywhere. We archive audit-proof, legally compliant and with a full audit trail.',
+ 'ocr_title' => 'Full-text search in seconds',
+ 'ocr_body' => 'Our full-text recognition (OCR) makes every page searchable. What used to mean leafing through an archive becomes a search of a few seconds.',
+ 'dms_title' => 'Setting up DMS & ECM',
+ 'dms_body' => 'We do not stop at scanning: on request we set up your document management (DMS/ECM), so the files land in a system that grows with you.',
+ 'pricing_title' => 'What does it cost to digitize your office?',
+ 'pricing_body' => 'Take a look at our price list.',
+ ],
+
+ 'about' => [
+ 'title' => 'About us',
+ 'subtitle' => 'A scanning centre in Zunzgen, not an anonymous corporation',
+ 'work_eyebrow' => 'What we do',
+ 'what_we_do_title' => 'Paper in, searchable documents out',
+ 'lead' => 'We digitize paper — from a single ring binder to an archive built up over decades. All of it happens here in Zunzgen: your documents never leave north-western Switzerland.',
+ 'what_title' => 'Preparing and scanning',
+ 'what_body' => 'We prepare your documents by hand, removing staples, tabs and sleeves, scan them, and run full-text recognition over the result. You get back files in which you find in seconds what used to mean leafing through folders.',
+ 'how_title' => 'Working traceably',
+ 'how_body' => 'We document every step, so the digitization stays audit-proof and legally compliant. On request we then destroy the originals with a certificate — or you get them back untouched.',
+ 'who_eyebrow' => 'Who is behind zunscan.ch',
+ 'who_title' => 'Two companies from the region',
+ 'who_lead' => 'zunscan.ch is run by paperflakes AG. Behind it stand two companies that complement each other — and at both of them you talk to a named contact directly.',
+ 'partners' => [
+ 'real-estate-club' => 'Takes care of everything on site in Zunzgen: premises, logistics and short distances to customers in the region.',
+ 'codebar' => 'Takes care of the technology behind it: text recognition, clean processes and document management that holds up.',
+ ],
+ ],
+
+ 'contact' => [
+ 'title' => 'Contact',
+ 'subtitle' => 'Happy to help, digitally and in person',
+ 'eyebrow' => 'Two companies, two contacts',
+ 'heading' => 'A joint venture with short distances',
+ 'body' => 'zunscan.ch is run by paperflakes AG — a joint venture of codebar Solutions AG and Real Estate Club GmbH. You reach a named contact at either partner directly, with no contact form in between.',
+ 'locations_title' => 'Locations',
+ 'write_to' => 'Send an e-mail to :name',
+ 'call' => 'Call :name',
+ 'linkedin_of' => 'LinkedIn profile of :name',
+ ],
+
+ 'media' => [
+ 'title' => 'Media',
+ 'subtitle' => 'Logos, news and coverage about zunscan.ch',
+ 'logo_alt' => 'zunscan.ch logo',
+ 'download' => 'Download logo',
+ ],
+
+ 'legal' => [
+ 'imprint_title' => 'Imprint',
+ 'imprint_subtitle' => 'Who operates this website',
+ 'privacy_title' => 'Privacy',
+ 'privacy_subtitle' => 'What happens to your data',
+ 'company_title' => 'Company',
+ 'uid' => 'VAT ID',
+ 'email' => 'E-mail',
+ ],
+
+ 'common' => [
+ 'price' => 'Price',
+ 'disposal' => 'Disposal',
+ 'service' => 'Service',
+ 'preparation' => 'Preparation',
+ 'remove_staples' => 'Removing staples',
+ 'remove_paperclips' => 'Removing paperclips',
+ 'remove_tabs' => 'Removing tabs',
+ 'remove_sleeves' => 'Removing plastic sleeves',
+ 'digitization_ocr' => 'Digitization incl. OCR',
+ ],
+
+ 'services' => [
+ 'scanning' => [
+ 'title' => 'Digitization',
+ 'subtitle' => 'We turn paper into pixels and folders into bytes using top-of-the-line equipment',
+ 'prices_eyebrow' => 'Transparent pricing',
+ 'prices_title' => 'Prices',
+ 'folder_a4_title' => 'Price per A4 ring binder',
+ 'folder_a4_pages' => 'Up to 430 pages',
+ 'folder_a4_price' => 'CHF 64.50',
+ 'folder_a4_disposal' => 'CHF 1.50 / kg',
+ 'page_a3a4_title' => 'Price per A3/A4 page',
+ 'page_a3a4_price' => 'CHF 0.19',
+ 'page_a3a4_disposal' => 'CHF 1.50 / kg',
+ 'flat_fee_title' => 'Order flat fee',
+ 'flat_fee_price' => 'CHF 499.00 for your first order',
+ 'more_services_title' => 'More services',
+ 'disposal_title' => 'Disposal',
+ 'disposal_subtitle' => 'You receive a destruction certificate from Reisswolf Aktenvernichtung AG.',
+ 'disposal_price_label' => 'Per destruction order',
+ 'disposal_price' => 'CHF 250.00',
+ 'return_title' => 'Return shipping',
+ 'return_subtitle' => 'You get your files back in their original condition.',
+ 'return_folder_label' => 'A4 folder',
+ 'return_folder_price' => 'CHF 108.50',
+ 'return_page_label' => 'A3/A4 page',
+ 'return_page_price' => 'CHF 0.31',
+ 'other_title' => 'Anything else',
+ 'other_subtitle' => 'Got a different format you would like digitized?',
+ 'other_price_label' => 'Billed by effort',
+ 'other_price' => 'CHF 90.00 / h',
+ ],
+ ],
+
+ 'cta' => [
+ 'heading' => 'Get in touch for a tailored offer',
+ 'button' => 'Contact us',
+ ],
+
+ 'footer' => [
+ 'heading' => 'Footer',
+ 'nav_title' => 'Navigation',
+ 'legal_title' => 'Legal',
+ 'copyright' => '© :year Zunscan. A project of :companies.',
+ 'swiss_digital_services' => 'swiss digital services',
+ ],
+
+];
diff --git a/package-lock.json b/package-lock.json
index 721e6c0..72348d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1657,9 +1657,9 @@
}
},
"node_modules/baseline-browser-mapping": {
- "version": "2.11.9",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.9.tgz",
- "integrity": "sha512-cp447VUsGS07+n1Dqf7YSQ8maeJrjEhaDxTm1ZefbqDtypHBC5GzGMQbklR6IPR13Y8OAJRHZWEMtZipJLCttg==",
+ "version": "2.11.12",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz",
+ "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
@@ -1968,9 +1968,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.399",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.399.tgz",
- "integrity": "sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA==",
+ "version": "1.5.401",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.401.tgz",
+ "integrity": "sha512-H6ViHN68nGYlChEvlIU67fn8O2/tpbWQPwck98yaJmh+08LSvHiydzDQ6oXNccLU3kNRVIRS9A4mA7CG+i6fLQ==",
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -2769,9 +2769,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.16",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
- "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+ "version": "3.3.17",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
+ "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==",
"funding": [
{
"type": "github",
@@ -2787,9 +2787,9 @@
}
},
"node_modules/node-releases": {
- "version": "2.0.51",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
- "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
+ "version": "2.0.52",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz",
+ "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -3136,9 +3136,9 @@
}
},
"node_modules/terser": {
- "version": "5.49.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz",
- "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==",
+ "version": "5.49.2",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.2.tgz",
+ "integrity": "sha512-rGbJiKeQ4WDe3EXlDAIaQcwftVfv2Q8o1awFNfvXolJYKkb1AuZY1RTOmqx4LJXZENbWZA7eIsYGHuEzHsi1nQ==",
"devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -3162,9 +3162,9 @@
"license": "MIT"
},
"node_modules/tinyexec": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
- "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
+ "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index da52616..2986f5b 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -45,5 +45,6 @@
+
diff --git a/public/favicons/zunscan/favicon.svg b/public/favicons/zunscan/favicon.svg
new file mode 100644
index 0000000..bb5154f
--- /dev/null
+++ b/public/favicons/zunscan/favicon.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/public/images/zunscan/sms-logo-v-black-services.png b/public/images/zunscan/sms-logo-v-black-services.png
new file mode 100644
index 0000000..11ccf1a
Binary files /dev/null and b/public/images/zunscan/sms-logo-v-black-services.png differ
diff --git a/public/images/zunscan/zunscan_cta_contact.svg b/public/images/zunscan/zunscan_cta_contact.svg
new file mode 100755
index 0000000..9ae88a7
--- /dev/null
+++ b/public/images/zunscan/zunscan_cta_contact.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/images/zunscan/zunscan_logo_pos.svg b/public/images/zunscan/zunscan_logo_pos.svg
new file mode 100755
index 0000000..c99f3d0
--- /dev/null
+++ b/public/images/zunscan/zunscan_logo_pos.svg
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/images/zunscan/zunscan_paperclip_half.svg b/public/images/zunscan/zunscan_paperclip_half.svg
new file mode 100755
index 0000000..3de671b
--- /dev/null
+++ b/public/images/zunscan/zunscan_paperclip_half.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/content/zunscan/de_CH/datenschutz.md b/resources/content/zunscan/de_CH/datenschutz.md
new file mode 100644
index 0000000..5217966
--- /dev/null
+++ b/resources/content/zunscan/de_CH/datenschutz.md
@@ -0,0 +1,19 @@
+## Datenschutzerklärung
+
+Die Bestimmungen berücksichtigen die Neuregelungen im Rahmen der Datenschutz-Grundverordnung der EU von 2018 (DSGVO
+2016/679). Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre
+personenbezogenen Daten vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften sowie dieser
+Datenschutzerklärung. Die Nutzung unserer Website ist in der Regel ohne Angabe personenbezogener Daten möglich. Soweit
+auf unseren Seiten personenbezogene Daten (beispielsweise Name, Anschrift oder E-Mail-Adressen) erhoben werden, erfolgt
+dies, soweit möglich, stets auf freiwilliger Basis. Diese Daten werden gemäss dieser Datenschutzerklärung teilweise an
+Dritte weitergegeben.
+
+---
+
+## SSL-Verschlüsselung
+
+Diese Seite nutzt aus Gründen der Sicherheit und zum Schutz der Übertragung vertraulicher Inhalte, wie zum Beispiel der
+Anfragen, die Sie an uns als Seitenbetreiber senden, eine SSL-Verschlüsselung. Eine verschlüsselte Verbindung erkennen
+Sie daran, dass die Adresszeile des Browsers von „http://" auf „https://" wechselt und an dem Schloss-Symbol in Ihrer
+Browserzeile. Wenn die SSL Verschlüsselung aktiviert ist, können die Daten, die Sie an uns übermitteln, nicht von
+Dritten mitgelesen werden.
diff --git a/resources/content/zunscan/de_CH/impressum.md b/resources/content/zunscan/de_CH/impressum.md
new file mode 100644
index 0000000..262c148
--- /dev/null
+++ b/resources/content/zunscan/de_CH/impressum.md
@@ -0,0 +1,20 @@
+## Haftungsausschluss
+
+Der Autor übernimmt keinerlei Gewähr hinsichtlich der inhaltlichen Richtigkeit, Genauigkeit, Aktualität, Zuverlässigkeit
+und Vollständigkeit der Informationen. Haftungsansprüche gegen den Autor wegen Schäden materieller oder immaterieller
+Art, welche aus dem Zugriff oder der Nutzung bzw. Nichtnutzung der veröffentlichten Informationen, durch Missbrauch der
+Verbindung oder durch technische Störungen entstanden sind, werden ausgeschlossen. Alle Angebote sind unverbindlich. Der
+Autor behält es sich ausdrücklich vor, Teile der Seiten oder das gesamte Angebot ohne gesonderte Ankündigung zu
+verändern, zu ergänzen, zu löschen oder die Veröffentlichung zeitweise oder endgültig einzustellen.
+
+## Haftung für Links
+
+Verweise und Links auf Webseiten Dritter liegen ausserhalb unseres Verantwortungsbereichs. Es wird jegliche
+Verantwortung für solche Webseiten abgelehnt. Zugriff und Nutzung solcher Webseiten erfolgen auf eigene Gefahr des
+Nutzers oder der Nutzerin.
+
+## Urheberrechte
+
+Die Urheber- und alle anderen Rechte an Inhalten auf der Website gehören ausschliesslich der paperflakes AG oder den
+speziell genannten Rechtsinhabern. Für die Reproduktion jeglicher Elemente ist die schriftliche Zustimmung der
+Urheberrechtsträger im Voraus einzuholen.
diff --git a/resources/content/zunscan/en_CH/datenschutz.md b/resources/content/zunscan/en_CH/datenschutz.md
new file mode 100644
index 0000000..c00a2a4
--- /dev/null
+++ b/resources/content/zunscan/en_CH/datenschutz.md
@@ -0,0 +1,17 @@
+## Privacy policy
+
+These provisions take into account the changes introduced under the EU's General Data Protection Regulation of 2018
+(GDPR 2016/679). The operators of these pages take the protection of your personal data very seriously. We treat your
+personal data confidentially and in accordance with statutory data protection regulations and this privacy policy. In
+general, our website can be used without providing personal data. Where personal data (such as name, address or email
+address) is collected on our pages, this is, wherever possible, always done on a voluntary basis. In accordance with
+this privacy policy, this data may in part be passed on to third parties.
+
+---
+
+## SSL encryption
+
+For security reasons and to protect the transmission of confidential content, such as requests you send to us as the
+site operator, this site uses SSL encryption. You can recognize an encrypted connection by the fact that the browser's
+address bar changes from "http://" to "https://" and by the padlock icon in your browser bar. When SSL encryption is
+active, the data you transmit to us cannot be read by third parties.
diff --git a/resources/content/zunscan/en_CH/impressum.md b/resources/content/zunscan/en_CH/impressum.md
new file mode 100644
index 0000000..c16e288
--- /dev/null
+++ b/resources/content/zunscan/en_CH/impressum.md
@@ -0,0 +1,18 @@
+## Disclaimer
+
+The author accepts no responsibility for the accuracy, correctness, timeliness, reliability or completeness of the
+information provided. Claims against the author relating to material or immaterial damage arising from access to, or
+use or non-use of, the published information, from misuse of the connection, or from technical faults are excluded.
+All offers are non-binding. The author expressly reserves the right to change, amend, delete or temporarily or
+permanently discontinue parts of the pages or the entire offer without prior notice.
+
+## Liability for links
+
+References and links to third-party websites lie outside our area of responsibility. Any liability for such websites
+is rejected. Access to and use of such websites is at the user's own risk.
+
+## Copyright
+
+The copyright and all other rights to content on this website belong exclusively to paperflakes AG or the specifically
+named rights holders. Written consent from the copyright holder must be obtained in advance for the reproduction of
+any elements.
diff --git a/resources/css/app.css b/resources/css/app.css
index d263079..46dd4c6 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -113,6 +113,8 @@
@source '../views';
@source '../js';
@source '../../app';
+@source not '../views/zunscan';
+@source not '../views/components/zunscan';
[x-cloak] {
display: none !important;
diff --git a/resources/css/zunscan.css b/resources/css/zunscan.css
new file mode 100644
index 0000000..dcd7595
--- /dev/null
+++ b/resources/css/zunscan.css
@@ -0,0 +1,198 @@
+@import 'tailwindcss' source(none);
+
+@theme {
+ --font-sans: 'Titillium Web', ui-sans-serif, system-ui, sans-serif;
+
+ /* The client's palette, unchanged. What changed is where each colour is
+ allowed to appear: paper is the canvas, white cards carry the content,
+ and blue is an accent that shows up at most twice per page (the CTA band
+ and the footer). dark-gray is a text colour only — it used to be a third
+ full-bleed surface under the CTA, which is what made the page read as
+ three unrelated colour systems stacked on top of each other. */
+ --color-zunscan-light-blue: #0093b8;
+ --color-zunscan-blue: #005c8a;
+ --color-zunscan-dark-blue: #16395a;
+ --color-zunscan-light-gray: #434f55;
+ --color-zunscan-dark-gray: #303b40;
+ --color-zunscan-white: #f5f5f5;
+
+ --background-image-paper: url('../images/zunscan/zunscan_papertexture.jpg');
+
+ /* ── Type scale ───────────────────────────────────────────────────────────
+ Fluid between 360px and 768px, so the responsive step lives in the token
+ and no heading needs an `sm:` variant to shrink on a phone. Replaces the
+ hand-written `text-4xl sm:text-6xl` pairs that were on every page and
+ never quite agreed with each other. Never put two of these on one
+ element — Tailwind picks the emission order, not the class string. */
+ --text-display: clamp(2rem, 1.45rem + 2.4vw, 3.5rem);
+ --text-display--line-height: 1.1;
+ --text-display--letter-spacing: -0.02em;
+ --text-display--font-weight: 700;
+
+ --text-title: clamp(1.5rem, 1.19rem + 1.37vw, 2.25rem);
+ --text-title--line-height: 1.2;
+ --text-title--letter-spacing: -0.015em;
+ --text-title--font-weight: 700;
+
+ --text-heading: clamp(1.25rem, 1.14rem + 0.49vw, 1.5rem);
+ --text-heading--line-height: 1.3;
+ --text-heading--font-weight: 700;
+
+ --text-lead: clamp(1.125rem, 1.06rem + 0.29vw, 1.375rem);
+ --text-lead--line-height: 1.5;
+
+ /* The uppercase kicker above a title. Titillium closes up when uppercased,
+ so the tracking belongs to the token rather than to each call site. */
+ --text-eyebrow: 0.8125rem;
+ --text-eyebrow--line-height: 1.4;
+ --text-eyebrow--letter-spacing: 0.12em;
+ --text-eyebrow--font-weight: 700;
+
+ /* ── Rhythm ───────────────────────────────────────────────────────────────
+ One vertical step between sections. Change this and the whole site
+ re-spaces; before this there were py-24 and py-16 in circulation with no
+ rule about which went where. */
+ --spacing-section: clamp(3rem, 2rem + 5vw, 6rem);
+
+ /* 44px — the WCAG 2.5.8 touch target, for nav links and the locale switch. */
+ --spacing-control: 2.75rem;
+
+ /* One radius and one shadow for every card on the site. */
+ --radius-card: 0.75rem;
+ --shadow-card: 0 12px 30px -18px rgb(22 57 90 / 0.35);
+}
+
+@source '../views/zunscan';
+@source '../views/components/zunscan';
+
+/* The imprint and privacy pages are the only long-form prose on the site, and
+ they need six rules rather than a whole typography plugin. Spacing lives on
+ one side only, so no two margins can collapse into an uneven rhythm. */
+.legal-prose {
+ line-height: 1.7;
+ color: var(--color-zunscan-light-gray);
+ font-weight: 300;
+}
+
+.legal-prose > * + * {
+ margin-block-start: 1.25rem;
+}
+
+.legal-prose h2 {
+ margin-block-start: 2.5rem;
+ font-size: var(--text-heading);
+ line-height: var(--text-heading--line-height);
+ font-weight: 700;
+ color: var(--color-zunscan-dark-gray);
+}
+
+.legal-prose > :first-child {
+ margin-block-start: 0;
+}
+
+.legal-prose h2 + * {
+ margin-block-start: 0.5rem;
+}
+
+.legal-prose a {
+ color: var(--color-zunscan-blue);
+ text-decoration: underline;
+ text-underline-offset: 0.2em;
+}
+
+.legal-prose strong {
+ font-weight: 700;
+ color: var(--color-zunscan-dark-gray);
+}
+
+@utility list-square {
+ list-style-type: square;
+}
+
+/* The header and footer band: the same paper texture as the content, carried on
+ the dark blue instead of on white.
+
+ `multiply` and not `overlay`: overlay doubles the base where the base is dark,
+ which lifted #16395a into a brighter, cornflower blue that is not in this
+ palette at all. multiply can only darken, so the surface stays exactly on the
+ brand navy and the speckle reads as grain rather than as a colour shift. */
+@utility bg-paper-dark {
+ background-color: var(--color-zunscan-dark-blue);
+ background-image: url('../images/zunscan/zunscan_papertexture.jpg');
+ background-size: cover;
+ background-position: center;
+ background-blend-mode: multiply;
+}
+
+/* The CTA band. Same construction as bg-paper-dark, one step lighter, so the
+ three coloured bands on a page are built the same way and only differ in
+ base colour — a flat gradient here read as a different material entirely.
+ zunscan-blue rather than light-blue: white on #005c8a measures 6.4:1 and
+ clears AA for the button label as well as the heading. */
+@utility bg-paper-blue {
+ background-color: var(--color-zunscan-blue);
+ background-image: url('../images/zunscan/zunscan_papertexture.jpg');
+ background-size: cover;
+ background-position: center;
+ background-blend-mode: multiply;
+}
+
+[x-cloak] {
+ display: none !important;
+}
+
+/* Self-hosted, weights 300/400/700 — the only ones this section's Blade views use
+ (font-light, font-normal, font-bold). Mirrors app.css's Poppins pattern. */
+@font-face {
+ font-family: 'Titillium Web';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url('../fonts/titillium-web/titillium-web-300-normal-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Titillium Web';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url('../fonts/titillium-web/titillium-web-300-normal-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Titillium Web';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url('../fonts/titillium-web/titillium-web-400-normal-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Titillium Web';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url('../fonts/titillium-web/titillium-web-400-normal-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Titillium Web';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url('../fonts/titillium-web/titillium-web-700-normal-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Titillium Web';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url('../fonts/titillium-web/titillium-web-700-normal-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
diff --git a/resources/fonts/titillium-web/titillium-web-300-normal-latin-ext.woff2 b/resources/fonts/titillium-web/titillium-web-300-normal-latin-ext.woff2
new file mode 100644
index 0000000..8adf0dd
Binary files /dev/null and b/resources/fonts/titillium-web/titillium-web-300-normal-latin-ext.woff2 differ
diff --git a/resources/fonts/titillium-web/titillium-web-300-normal-latin.woff2 b/resources/fonts/titillium-web/titillium-web-300-normal-latin.woff2
new file mode 100644
index 0000000..9195e31
Binary files /dev/null and b/resources/fonts/titillium-web/titillium-web-300-normal-latin.woff2 differ
diff --git a/resources/fonts/titillium-web/titillium-web-400-normal-latin-ext.woff2 b/resources/fonts/titillium-web/titillium-web-400-normal-latin-ext.woff2
new file mode 100644
index 0000000..ee4317c
Binary files /dev/null and b/resources/fonts/titillium-web/titillium-web-400-normal-latin-ext.woff2 differ
diff --git a/resources/fonts/titillium-web/titillium-web-400-normal-latin.woff2 b/resources/fonts/titillium-web/titillium-web-400-normal-latin.woff2
new file mode 100644
index 0000000..4750196
Binary files /dev/null and b/resources/fonts/titillium-web/titillium-web-400-normal-latin.woff2 differ
diff --git a/resources/fonts/titillium-web/titillium-web-700-normal-latin-ext.woff2 b/resources/fonts/titillium-web/titillium-web-700-normal-latin-ext.woff2
new file mode 100644
index 0000000..02a31b8
Binary files /dev/null and b/resources/fonts/titillium-web/titillium-web-700-normal-latin-ext.woff2 differ
diff --git a/resources/fonts/titillium-web/titillium-web-700-normal-latin.woff2 b/resources/fonts/titillium-web/titillium-web-700-normal-latin.woff2
new file mode 100644
index 0000000..8672f69
Binary files /dev/null and b/resources/fonts/titillium-web/titillium-web-700-normal-latin.woff2 differ
diff --git a/resources/images/zunscan/zunscan_papertexture.jpg b/resources/images/zunscan/zunscan_papertexture.jpg
new file mode 100755
index 0000000..3d2fddc
Binary files /dev/null and b/resources/images/zunscan/zunscan_papertexture.jpg differ
diff --git a/resources/js/zunscan.js b/resources/js/zunscan.js
new file mode 100644
index 0000000..d7a509c
--- /dev/null
+++ b/resources/js/zunscan.js
@@ -0,0 +1,6 @@
+import '../css/zunscan.css'
+
+import Alpine from '@alpinejs/csp'
+
+window.Alpine = Alpine
+Alpine.start()
diff --git a/resources/views/components/zunscan/components/blue.blade.php b/resources/views/components/zunscan/components/blue.blade.php
new file mode 100644
index 0000000..a932650
--- /dev/null
+++ b/resources/views/components/zunscan/components/blue.blade.php
@@ -0,0 +1,10 @@
+
+ {{ $cta ?? '' }}
+
+
+
diff --git a/resources/views/components/zunscan/components/card.blade.php b/resources/views/components/zunscan/components/card.blade.php
new file mode 100644
index 0000000..b7a34f8
--- /dev/null
+++ b/resources/views/components/zunscan/components/card.blade.php
@@ -0,0 +1,6 @@
+{{-- One radius and one shadow, defined once. Every white surface on the site
+ is this component — cards that set their own rounding are how the old
+ layout ended up with three different corner sizes on one page. --}}
+merge(['class' => 'rounded-card bg-white p-6 shadow-card']) }}>
+ {{ $slot }}
+
diff --git a/resources/views/components/zunscan/components/eyebrow.blade.php b/resources/views/components/zunscan/components/eyebrow.blade.php
new file mode 100644
index 0000000..a011969
--- /dev/null
+++ b/resources/views/components/zunscan/components/eyebrow.blade.php
@@ -0,0 +1,5 @@
+{{-- The small uppercase kicker above a title. `uppercase` stays a utility —
+ the text-* namespace cannot carry text-transform. --}}
+merge(['class' => 'text-eyebrow uppercase text-zunscan-light-blue']) }}>
+ {{ $slot }}
+
diff --git a/resources/views/components/zunscan/components/icon.blade.php b/resources/views/components/zunscan/components/icon.blade.php
new file mode 100644
index 0000000..cafa10f
--- /dev/null
+++ b/resources/views/components/zunscan/components/icon.blade.php
@@ -0,0 +1,48 @@
+@props(['name'])
+
+{{-- Inline outline icons, same drawing style as the chevrons in the header.
+ Inline rather than a sprite or an icon font: a handful of icons does not
+ justify either, and inline SVG needs no CSP allowance.
+ LinkedIn is the exception — a brand mark is a filled shape on its own
+ 16-unit grid, so it cannot share the stroked 24-unit wrapper. --}}
+@if($name === 'linkedin')
+ merge(['class' => 'h-5 w-5']) }}
+ fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
+
+
+@else
+ merge(['class' => 'h-7 w-7 text-zunscan-light-blue']) }}
+ fill="none" viewBox="0 0 24 24" stroke-width="1.6" stroke="currentColor" aria-hidden="true">
+ @switch($name)
+ @case('space')
+
+ @break
+
+ @case('mail')
+
+ @break
+
+ @case('phone')
+
+ @break
+
+ @case('shield')
+
+ @break
+
+ @case('system')
+
+ @break
+
+ @case('search')
+
+ @break
+ @endswitch
+
+@endif
diff --git a/resources/views/components/zunscan/components/paper.blade.php b/resources/views/components/zunscan/components/paper.blade.php
new file mode 100644
index 0000000..a65b032
--- /dev/null
+++ b/resources/views/components/zunscan/components/paper.blade.php
@@ -0,0 +1,5 @@
+
diff --git a/resources/views/components/zunscan/components/person.blade.php b/resources/views/components/zunscan/components/person.blade.php
new file mode 100644
index 0000000..723cca6
--- /dev/null
+++ b/resources/views/components/zunscan/components/person.blade.php
@@ -0,0 +1,58 @@
+@props([
+ 'name',
+ 'company',
+ 'website',
+ 'websiteLabel',
+ 'email',
+ 'phone',
+ 'linkedin' => null,
+ 'image' => null,
+])
+
+{{-- The main site's x-card.person-card is deliberately not reused here: it is
+ bound to that site's tokens, and pulling it in would put Poppins and the
+ brand purple into this bundle. CloudinaryUrl is fine to share — it is a URL
+ builder with no design opinion. --}}
+
+ @if($image)
+
+ @endif
+
+
+
{{ $name }}
+
{{ $company }}
+
+
{{ $websiteLabel }}
+
+ {{-- Channels as icons rather than repeated blue text lines: the address
+ itself is not the point, reaching the person is. Each carries an
+ accessible name, since an icon alone is not a label. --}}
+
+
+
diff --git a/resources/views/components/zunscan/components/section.blade.php b/resources/views/components/zunscan/components/section.blade.php
new file mode 100644
index 0000000..a1062ba
--- /dev/null
+++ b/resources/views/components/zunscan/components/section.blade.php
@@ -0,0 +1,16 @@
+@props(['tone' => 'paper'])
+
+{{--
+ The one vertical rhythm on the site. `tone` picks the surface: paper is the
+ default canvas, blue is the accent band and must not appear more than once
+ on a page besides the footer.
+--}}
+ $tone === 'paper',
+ 'bg-paper-blue' => $tone === 'blue',
+ 'bg-white' => $tone === 'white',
+])>
+
merge(['class' => 'mx-auto max-w-5xl px-6 py-section']) }}>
+ {{ $slot }}
+
+
diff --git a/resources/views/components/zunscan/components/title.blade.php b/resources/views/components/zunscan/components/title.blade.php
new file mode 100644
index 0000000..364c5d1
--- /dev/null
+++ b/resources/views/components/zunscan/components/title.blade.php
@@ -0,0 +1,33 @@
+@props([
+ 'title',
+ 'subtitle' => null,
+ 'eyebrow' => null,
+])
+
+{{--
+ The header band is its own surface, not a rule drawn across the same one.
+ Hero and content used to share the paper texture, so no divider could carry
+ enough weight to separate them. Now the page reads as three zones:
+ dark-blue header → paper content → blue footer.
+
+ Colour choices are contrast-driven, not decorative: the subtitle is white at
+ 80% rather than light-blue, because #0093b8 on #16395a measures about 3:1 and
+ would fail AA for text this size.
+--}}
+
+
+
+
+
+ @if($eyebrow)
+
{{ $eyebrow }}
+ @endif
+
+
{{ $title }}
+
+ @if($subtitle)
+
{{ $subtitle }}
+ @endif
+
+
+
diff --git a/resources/views/components/zunscan/layout.blade.php b/resources/views/components/zunscan/layout.blade.php
new file mode 100644
index 0000000..ffd8177
--- /dev/null
+++ b/resources/views/components/zunscan/layout.blade.php
@@ -0,0 +1,80 @@
+@props([
+ 'title',
+ 'description',
+ 'robots' => 'index,follow',
+ 'image' => null,
+])
+
+@php
+ // Built from the request, never from config('app.url'): AppServiceProvider
+ // pins that to the main site's host for the whole app, and the Zunscan
+ // middleware only corrects Laravel's URL generator — request()->url() is
+ // the honest answer for this domain either way. Query strings are dropped
+ // so a ?utm_* variant does not become its own canonical.
+ $canonical = request()->url();
+ $locale = str_replace('_', '-', app()->getLocale());
+@endphp
+
+
+
+
+
+
+
+ {{ $title }} | zunscan.ch
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if($image)
+ {{-- Dimensions are declared so a crawler can lay the card out before the
+ image has downloaded; without them the preview often falls back to a
+ small thumbnail. The Cloudinary transform pins the same 1200×630. --}}
+
+
+
+
+
+
+
+
+
+
+ @endif
+
+ @foreach(\App\Enums\LocaleEnum::cases() as $alternate)
+
+ @endforeach
+ {{-- Same target as the main site's x-default: the German start page. --}}
+
+
+ {{-- Zunscan had no favicon at all, so every page logged a 404 for
+ /favicon.ico. The logo is already an SVG, so it doubles as the icon. --}}
+
+
+
+
+
+
+ @vite(['resources/js/zunscan.js'])
+
+
+
+{{ $slot }}
+
+
+
+
diff --git a/resources/views/components/zunscan/patials/contactcta.blade.php b/resources/views/components/zunscan/patials/contactcta.blade.php
new file mode 100644
index 0000000..33f094e
--- /dev/null
+++ b/resources/views/components/zunscan/patials/contactcta.blade.php
@@ -0,0 +1,17 @@
+{{-- The one blue band on a content page. It used to be a grey gradient, which
+ introduced a third colour family that appeared nowhere else on the site. --}}
+
+
+
diff --git a/resources/views/components/zunscan/patials/footer.blade.php b/resources/views/components/zunscan/patials/footer.blade.php
new file mode 100644
index 0000000..29a9d82
--- /dev/null
+++ b/resources/views/components/zunscan/patials/footer.blade.php
@@ -0,0 +1,85 @@
+@php
+ // Both owners, linked, joined with an ampersand. Built here rather than
+ // baked into the translation so the string stays one natural sentence and
+ // the company list has a single source in config.
+ $owners = collect(config('zunscan.people'))
+ ->map(fn (array $person): string => sprintf(
+ '%s ',
+ e($person['website']),
+ e($person['company']),
+ ))
+ ->implode(' & ');
+@endphp
+
+{{-- Same dark textured surface as the header, so the page bookends:
+ dark header → paper content → one bright blue CTA band → dark footer.
+ The footer used to carry the identical gradient as the CTA directly above
+ it, which made the boundary between them read as a rendering seam. --}}
+
diff --git a/resources/views/components/zunscan/patials/header.blade.php b/resources/views/components/zunscan/patials/header.blade.php
new file mode 100644
index 0000000..db10757
--- /dev/null
+++ b/resources/views/components/zunscan/patials/header.blade.php
@@ -0,0 +1,72 @@
+
+
+
+
+
+ {{ __('zunscan.nav.open_menu') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/views/components/zunscan/patials/locale-switch.blade.php b/resources/views/components/zunscan/patials/locale-switch.blade.php
new file mode 100644
index 0000000..2be7a9f
--- /dev/null
+++ b/resources/views/components/zunscan/patials/locale-switch.blade.php
@@ -0,0 +1,15 @@
+
diff --git a/resources/views/components/zunscan/patials/schema.blade.php b/resources/views/components/zunscan/patials/schema.blade.php
new file mode 100644
index 0000000..1fed937
--- /dev/null
+++ b/resources/views/components/zunscan/patials/schema.blade.php
@@ -0,0 +1,66 @@
+@props(['title', 'description', 'canonical'])
+
+@php
+ // A deliberately small graph. The main site's SchemaGraph is not reused: it
+ // is built around codebar's own config/company.php and would describe the
+ // wrong organisation on this domain.
+ $root = request()->getSchemeAndHttpHost();
+
+ $company = config('zunscan.company');
+
+ $graph = [
+ [
+ '@type' => 'Organization',
+ '@id' => $root.'/#organization',
+ 'name' => 'zunscan.ch',
+ 'legalName' => $company['legal_name'],
+ 'url' => $root.'/',
+ 'email' => $company['email'],
+ 'vatID' => $company['uid'],
+ 'address' => [
+ '@type' => 'PostalAddress',
+ 'streetAddress' => $company['street'],
+ 'postalCode' => $company['postal_code'],
+ 'addressLocality' => $company['locality'],
+ 'addressCountry' => 'CH',
+ ],
+ // The joint venture, stated in the markup as well as in the copy.
+ 'parentOrganization' => array_map(fn (array $person): array => [
+ '@type' => 'Organization',
+ 'name' => $person['company'],
+ 'url' => $person['website'],
+ ], config('zunscan.people')),
+ ],
+ [
+ '@type' => 'WebSite',
+ '@id' => $root.'/#website',
+ 'url' => $root.'/',
+ 'name' => 'zunscan.ch',
+ 'inLanguage' => str_replace('_', '-', app()->getLocale()),
+ 'publisher' => ['@id' => $root.'/#organization'],
+ ],
+ [
+ '@type' => 'WebPage',
+ '@id' => $canonical.'#webpage',
+ 'url' => $canonical,
+ 'name' => $title,
+ 'description' => $description,
+ 'inLanguage' => str_replace('_', '-', app()->getLocale()),
+ 'isPartOf' => ['@id' => $root.'/#website'],
+ ],
+ ];
+
+ $payload = json_encode([
+ '@context' => 'https://schema.org',
+ '@graph' => $graph,
+ ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+@endphp
+
+{{--
+ Same handling as the main site's layouts/_partials/_schema.blade.php: a
+ sequence only — this payload comes from our own config, never from
+ request input.
+--}}
+
diff --git a/resources/views/components/zunscan/services/container.blade.php b/resources/views/components/zunscan/services/container.blade.php
new file mode 100644
index 0000000..bacded4
--- /dev/null
+++ b/resources/views/components/zunscan/services/container.blade.php
@@ -0,0 +1,16 @@
+@props([
+ 'title',
+ 'subtitle' => null,
+])
+
+
+
{{ $title }}
+
+ @if($subtitle)
+
+ {{ $subtitle }}
+
+ @endif
+
+
+{{ $slot }}
diff --git a/resources/views/zunscan/about/index.blade.php b/resources/views/zunscan/about/index.blade.php
new file mode 100644
index 0000000..26faf5c
--- /dev/null
+++ b/resources/views/zunscan/about/index.blade.php
@@ -0,0 +1,58 @@
+@php
+ $work = [
+ ['icon' => 'space', 'title' => 'zunscan.about.what_title', 'body' => 'zunscan.about.what_body'],
+ ['icon' => 'shield', 'title' => 'zunscan.about.how_title', 'body' => 'zunscan.about.how_body'],
+ ];
+
+ // Company and link come from the same config the contact page uses, so the
+ // two never drift apart; only the blurb is translated, keyed by company.
+ $partners = config('zunscan.people');
+@endphp
+
+
+
+
+
+ {{-- What we do comes first, who we are second: the eyebrow used to
+ promise «who is behind zunscan.ch» and then open with what we do. --}}
+ {{ __('zunscan.about.work_eyebrow') }}
+ {{ __('zunscan.about.what_we_do_title') }}
+ {{ __('zunscan.about.lead') }}
+
+
+ @foreach($work as $block)
+
+
+ {{ __($block['title']) }}
+ {{ __($block['body']) }}
+
+ @endforeach
+
+
+
+
{{ __('zunscan.about.who_eyebrow') }}
+
{{ __('zunscan.about.who_title') }}
+
{{ __('zunscan.about.who_lead') }}
+
+
+
+
+
+
+
diff --git a/resources/views/zunscan/contact/index.blade.php b/resources/views/zunscan/contact/index.blade.php
new file mode 100644
index 0000000..0976fd8
--- /dev/null
+++ b/resources/views/zunscan/contact/index.blade.php
@@ -0,0 +1,38 @@
+
+
+
+
+ {{ __('zunscan.contact.eyebrow') }}
+ {{ __('zunscan.contact.heading') }}
+ {{ __('zunscan.contact.body') }}
+
+
+ @foreach(config('zunscan.people') as $person)
+
+ @endforeach
+
+
+ {{ __('zunscan.contact.locations_title') }}
+
+
+ @foreach(config('zunscan.locations') as $location)
+
+ {{ $location['company'] }}
+
+ {{ $location['street'] }}
+ {{ $location['city'] }}
+
+
+ @endforeach
+
+
+
diff --git a/resources/views/zunscan/legal/privacy.blade.php b/resources/views/zunscan/legal/privacy.blade.php
new file mode 100644
index 0000000..d6fc8f2
--- /dev/null
+++ b/resources/views/zunscan/legal/privacy.blade.php
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+ {!! $body !!}
+
+
+
+
diff --git a/resources/views/zunscan/legal/terms.blade.php b/resources/views/zunscan/legal/terms.blade.php
new file mode 100644
index 0000000..e0a1e42
--- /dev/null
+++ b/resources/views/zunscan/legal/terms.blade.php
@@ -0,0 +1,44 @@
+@php
+ $company = config('zunscan.company');
+@endphp
+
+
+
+
+
+ {{-- Narrower than the section: legal text is long-form prose and a 60rem
+ measure is roughly twice a comfortable reading width. --}}
+
+ {{-- Rendered from config rather than from the markdown. Single newlines
+ collapse to spaces in CommonMark, so an address written as markdown
+ came out as one run-on line, and it was a second place the address
+ had to be kept correct. --}}
+
+ {{ __('zunscan.legal.company_title') }}
+
+
+ {{ $company['legal_name'] }}
+ {{ $company['street'] }}
+ CH-{{ $company['postal_code'] }} {{ $company['locality'] }}
+
+
+
+ {{ __('zunscan.legal.uid') }}
+ {{ $company['uid'] }}
+
+ {{ __('zunscan.legal.email') }}
+
+ {{ $company['email'] }}
+
+
+
+
+
+
+ {!! $body !!}
+
+
+
+
+
diff --git a/resources/views/zunscan/media/index.blade.php b/resources/views/zunscan/media/index.blade.php
new file mode 100644
index 0000000..ea63aab
--- /dev/null
+++ b/resources/views/zunscan/media/index.blade.php
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/resources/views/zunscan/services/show/scanning.blade.php b/resources/views/zunscan/services/show/scanning.blade.php
new file mode 100644
index 0000000..ba539b3
--- /dev/null
+++ b/resources/views/zunscan/services/show/scanning.blade.php
@@ -0,0 +1,126 @@
+@php
+ $extras = [
+ [
+ 'title' => 'zunscan.services.scanning.disposal_title',
+ 'subtitle' => 'zunscan.services.scanning.disposal_subtitle',
+ 'rows' => [
+ ['zunscan.services.scanning.disposal_price_label', 'zunscan.services.scanning.disposal_price'],
+ ],
+ ],
+ [
+ 'title' => 'zunscan.services.scanning.return_title',
+ 'subtitle' => 'zunscan.services.scanning.return_subtitle',
+ 'rows' => [
+ ['zunscan.services.scanning.return_folder_label', 'zunscan.services.scanning.return_folder_price'],
+ ['zunscan.services.scanning.return_page_label', 'zunscan.services.scanning.return_page_price'],
+ ],
+ ],
+ [
+ 'title' => 'zunscan.services.scanning.other_title',
+ 'subtitle' => 'zunscan.services.scanning.other_subtitle',
+ 'rows' => [
+ ['zunscan.services.scanning.other_price_label', 'zunscan.services.scanning.other_price'],
+ ],
+ ],
+ ];
+@endphp
+
+
+
+
+
+ {{ __('zunscan.services.scanning.prices_eyebrow') }}
+ {{ __('zunscan.services.scanning.prices_title') }}
+
+
+
+ {{ __('zunscan.services.scanning.folder_a4_title') }}
+
+
+
+ {{ __('zunscan.services.scanning.folder_a4_pages') }}
+
+ {{ __('zunscan.common.preparation') }}
+
+ {{ __('zunscan.common.remove_staples') }}
+ {{ __('zunscan.common.remove_paperclips') }}
+ {{ __('zunscan.common.remove_tabs') }}
+ {{ __('zunscan.common.remove_sleeves') }}
+
+
+ {{ __('zunscan.common.digitization_ocr') }}
+
+
+
+
+
+
{{ __('zunscan.common.price') }}
+ {{ __('zunscan.services.scanning.folder_a4_price') }}
+
+
+
{{ __('zunscan.common.disposal') }}
+ {{ __('zunscan.services.scanning.folder_a4_disposal') }}
+
+
+
+
+
+ {{ __('zunscan.services.scanning.page_a3a4_title') }}
+
+
+
+
+ {{ __('zunscan.common.preparation') }}
+
+ {{ __('zunscan.common.remove_staples') }}
+ {{ __('zunscan.common.remove_paperclips') }}
+ {{ __('zunscan.common.remove_tabs') }}
+ {{ __('zunscan.common.remove_sleeves') }}
+
+
+ {{ __('zunscan.common.digitization_ocr') }}
+
+
+
+
+
+
{{ __('zunscan.common.price') }}
+ {{ __('zunscan.services.scanning.page_a3a4_price') }}
+
+
+
{{ __('zunscan.common.disposal') }}
+ {{ __('zunscan.services.scanning.page_a3a4_disposal') }}
+
+
+
+
+
+ {{ __('zunscan.services.scanning.flat_fee_title') }}
+ {{ __('zunscan.services.scanning.flat_fee_price') }}
+
+
+
+ {{ __('zunscan.services.scanning.more_services_title') }}
+
+
+ @foreach($extras as $extra)
+
+ {{ __($extra['title']) }}
+ {{ __($extra['subtitle']) }}
+
+
+ @foreach($extra['rows'] as [$label, $price])
+
+
{{ __($label) }}
+ {{ __($price) }}
+
+ @endforeach
+
+
+ @endforeach
+
+
+
+
+
diff --git a/resources/views/zunscan/start/index.blade.php b/resources/views/zunscan/start/index.blade.php
new file mode 100644
index 0000000..5a8f018
--- /dev/null
+++ b/resources/views/zunscan/start/index.blade.php
@@ -0,0 +1,56 @@
+@php
+ // Four cards in a 2×2 grid, and the bodies are written to roughly equal
+ // length on purpose — uneven copy is what made the row look ragged.
+ $solutions = [
+ ['icon' => 'space', 'title' => 'zunscan.start.space_title', 'body' => 'zunscan.start.space_body'],
+ // Post-Scanning-Dienst: deaktiviert, solange ePost nicht publiziert ist —
+ // die Kachel bewarb eine Dienstleistung, die keine Seite mehr hat.
+ // Wieder aktivieren, sobald die ePost-Seite zurückkommt (die Texte
+ // liegen weiterhin unter zunscan.start.mail_* in beiden Sprachdateien).
+ // ['icon' => 'mail', 'title' => 'zunscan.start.mail_title', 'body' => 'zunscan.start.mail_body'],
+ ['icon' => 'shield', 'title' => 'zunscan.start.compliance_title', 'body' => 'zunscan.start.compliance_body'],
+ ['icon' => 'search', 'title' => 'zunscan.start.ocr_title', 'body' => 'zunscan.start.ocr_body'],
+ ['icon' => 'system', 'title' => 'zunscan.start.dms_title', 'body' => 'zunscan.start.dms_body'],
+ ];
+@endphp
+
+
+
+
+
+ {{ __('zunscan.start.solutions_eyebrow') }}
+ {{ __('zunscan.start.solutions_title') }}
+
+
+ @foreach($solutions as $solution)
+
+
+ {{-- Two lines reserved for the title so the body copy starts on
+ the same baseline in all three cards — «Platzersparnis» is
+ one line, the other two wrap. --}}
+ {{ __($solution['title']) }}
+ {{ __($solution['body']) }}
+
+ @endforeach
+
+
+
+
+
{{ __('zunscan.start.pricing_title') }}
+
{{ __('zunscan.start.pricing_body') }}
+
+
+
+ {{ __('zunscan.nav.scanning') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/routes/web.php b/routes/web.php
index 261a2c6..51340bc 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -42,6 +42,10 @@
$locales = array_column(LocaleEnum::cases(), 'value');
+// Registered first: its own robots.txt/sitemap.xml routes (in routes/zunscan.php)
+// must win over the unscoped ones below, which have no domain constraint.
+Route::domain(config()->string('zunscan.domain'))->group(base_path('routes/zunscan.php'));
+
Route::get('/', EntryIndexController::class)->name('entry.index');
Route::group(['as' => Str::slug(LocaleEnum::EN->value).'.'], function () use ($locales) {
diff --git a/routes/zunscan.php b/routes/zunscan.php
new file mode 100644
index 0000000..aff18de
--- /dev/null
+++ b/routes/zunscan.php
@@ -0,0 +1,49 @@
+group(function () {
+ // The domain root was never a language choice, same reasoning as the main
+ // site's EntryIndexController — German is the default, always, not
+ // whatever a session happens to remember. Registered inside this group,
+ // not above it, so SetZunscanLanguage has already corrected the forced
+ // root URL to this domain before the redirect target is built.
+ Route::redirect('/', '/de-ch', 301);
+
+ Route::group(['as' => 'zunscan.de-ch.'], function () {
+ Route::get('de-ch', StartController::class)->name('start.index');
+ Route::get('de-ch/about', AboutController::class)->name('about.index');
+ Route::get('de-ch/services/scanning', ServicesScanningController::class)->name('services.scanning.show');
+ Route::get('de-ch/kontakt', ContactController::class)->name('contact.index');
+ Route::get('de-ch/medien', MediaController::class)->name('media.index');
+ Route::get('de-ch/impressum', TermsController::class)->name('terms.index');
+ Route::get('de-ch/datenschutz', PrivacyController::class)->name('privacy.index');
+ });
+
+ Route::group(['as' => 'zunscan.en-ch.'], function () {
+ Route::get('en-ch', StartController::class)->name('start.index');
+ Route::get('en-ch/about', AboutController::class)->name('about.index');
+ Route::get('en-ch/services/scanning', ServicesScanningController::class)->name('services.scanning.show');
+ Route::get('en-ch/contact', ContactController::class)->name('contact.index');
+ Route::get('en-ch/media', MediaController::class)->name('media.index');
+ Route::get('en-ch/imprint', TermsController::class)->name('terms.index');
+ Route::get('en-ch/privacy', PrivacyController::class)->name('privacy.index');
+ });
+});
diff --git a/tests/Feature/Zunscan/ZunscanPagesTest.php b/tests/Feature/Zunscan/ZunscanPagesTest.php
new file mode 100644
index 0000000..fc38ceb
--- /dev/null
+++ b/tests/Feature/Zunscan/ZunscanPagesTest.php
@@ -0,0 +1,209 @@
+assertRedirect('https://zunscan.codebar.ch/de-ch');
+})->group('zunscan');
+
+it('serves every zunscan.codebar.ch page in german', function (string $path) {
+ get('http://zunscan.codebar.ch'.$path)->assertOk();
+})->with([
+ '/de-ch',
+ '/de-ch/about',
+ '/de-ch/services/scanning',
+ '/de-ch/kontakt',
+ '/de-ch/medien',
+ '/de-ch/impressum',
+ '/de-ch/datenschutz',
+])->group('zunscan');
+
+it('serves every zunscan.codebar.ch page in english', function (string $path) {
+ get('http://zunscan.codebar.ch'.$path)->assertOk();
+})->with([
+ '/en-ch',
+ '/en-ch/about',
+ '/en-ch/services/scanning',
+ '/en-ch/contact',
+ '/en-ch/media',
+ '/en-ch/imprint',
+ '/en-ch/privacy',
+])->group('zunscan');
+
+it('does not publish the epost service', function () {
+ get('http://zunscan.codebar.ch/de-ch/services/epost')->assertNotFound();
+ get('http://zunscan.codebar.ch/en-ch/services/epost')->assertNotFound();
+})->group('zunscan');
+
+it('renders german content on the de-ch routes', function () {
+ $response = get('http://zunscan.codebar.ch/de-ch');
+
+ $response->assertOk();
+ $response->assertSee('Herzlich Willkommen');
+ $response->assertSee('Kontakt');
+})->group('zunscan');
+
+it('renders english content on the en-ch routes', function () {
+ $response = get('http://zunscan.codebar.ch/en-ch');
+
+ $response->assertOk();
+ $response->assertSee('Welcome');
+ $response->assertSee('Contact');
+})->group('zunscan');
+
+it('does not serve zunscan pages on the main domain', function () {
+ get('http://web.codebar.test/services/scanning')->assertNotFound();
+})->group('zunscan');
+
+it('serves a zunscan-specific robots.txt', function () {
+ app()->detectEnvironment(fn (): string => 'production');
+
+ $response = get('http://zunscan.codebar.ch/robots.txt');
+
+ $response->assertOk();
+ $response->assertHeader('Content-Type', 'text/plain; charset=UTF-8');
+ expect($response->getContent())->toContain('Sitemap: http://zunscan.codebar.ch/sitemap.xml');
+})->group('zunscan');
+
+it('serves a zunscan-specific sitemap scoped to its own domain and both locales', function () {
+ $response = get('http://zunscan.codebar.ch/sitemap.xml');
+
+ $response->assertOk();
+ $response->assertHeader('Content-Type', 'application/xml');
+
+ $content = $response->getContent();
+
+ expect($content)->toContain('http://zunscan.codebar.ch/de-ch');
+ expect($content)->toContain('http://zunscan.codebar.ch/en-ch');
+ expect($content)->not->toContain('services/epost');
+ expect($content)->not->toContain('www.codebar.ch');
+})->group('zunscan');
+
+it('links to the other language via the locale switcher', function () {
+ $response = get('http://zunscan.codebar.ch/de-ch/kontakt');
+
+ $response->assertOk();
+ $response->assertSee('href="http://zunscan.codebar.ch/en-ch/contact"', false);
+})->group('zunscan');
+
+it('links back to the german page from its english counterpart', function () {
+ $response = get('http://zunscan.codebar.ch/en-ch/contact');
+
+ $response->assertOk();
+ $response->assertSee('href="http://zunscan.codebar.ch/de-ch/kontakt"', false);
+})->group('zunscan');
+
+it('serves zunscan CSS and JS from the requesting host, not the main site domain', function () {
+ $response = get('http://zunscan.codebar.ch/de-ch');
+
+ $response->assertOk();
+ $response->assertSee('zunscan.codebar.ch/build/assets/', false);
+ $response->assertDontSee((string) parse_url(config()->string('app.url'), PHP_URL_HOST), false);
+})->group('zunscan');
+
+it('renders a canonical url on the requesting host', function () {
+ $response = get('http://zunscan.codebar.ch/de-ch/kontakt');
+
+ $response->assertOk();
+ $response->assertSee(' ', false);
+})->group('zunscan');
+
+it('points x-default at the german start page', function () {
+ $response = get('http://zunscan.codebar.ch/en-ch');
+
+ $response->assertOk();
+ $response->assertSee('hreflang="x-default"', false);
+ $response->assertSee('href="http://zunscan.codebar.ch/de-ch"', false);
+})->group('zunscan');
+
+it('suffixes the page title with the brand', function () {
+ get('http://zunscan.codebar.ch/de-ch/kontakt')
+ ->assertOk()
+ ->assertSee('Kontakt | zunscan.ch ', false);
+})->group('zunscan');
+
+it('emits a valid json-ld graph describing the joint venture', function () {
+ $html = (string) get('http://zunscan.codebar.ch/de-ch')->assertOk()->getContent();
+
+ expect(preg_match('/