From ddd5949595bbd4a41eb2f9da0eb28f05be9c38fd Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Thu, 1 Oct 2026 11:05:35 +0100 Subject: [PATCH 1/3] new terms of service and refunds --- AGENTS.md => docs/AGENTS.md | 0 CLAUDE.md => docs/CLAUDE.md | 0 .env.develop.example => provision/.env.develop.example | 0 .env.paddle => provision/.env.paddle | 0 .env.production.example => provision/.env.production.example | 0 .env.stage.example => provision/.env.stage.example | 0 sonar_desk | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename AGENTS.md => docs/AGENTS.md (100%) rename CLAUDE.md => docs/CLAUDE.md (100%) rename .env.develop.example => provision/.env.develop.example (100%) rename .env.paddle => provision/.env.paddle (100%) rename .env.production.example => provision/.env.production.example (100%) rename .env.stage.example => provision/.env.stage.example (100%) delete mode 100644 sonar_desk diff --git a/AGENTS.md b/docs/AGENTS.md similarity index 100% rename from AGENTS.md rename to docs/AGENTS.md diff --git a/CLAUDE.md b/docs/CLAUDE.md similarity index 100% rename from CLAUDE.md rename to docs/CLAUDE.md diff --git a/.env.develop.example b/provision/.env.develop.example similarity index 100% rename from .env.develop.example rename to provision/.env.develop.example diff --git a/.env.paddle b/provision/.env.paddle similarity index 100% rename from .env.paddle rename to provision/.env.paddle diff --git a/.env.production.example b/provision/.env.production.example similarity index 100% rename from .env.production.example rename to provision/.env.production.example diff --git a/.env.stage.example b/provision/.env.stage.example similarity index 100% rename from .env.stage.example rename to provision/.env.stage.example diff --git a/sonar_desk b/sonar_desk deleted file mode 100644 index e69de29..0000000 From a94f2941843d3481ad063cc47337952183dc1020 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Thu, 1 Oct 2026 11:08:19 +0100 Subject: [PATCH 2/3] new terms of service and refunds --- .gitignore | 1 + .scannerwork/.sonar_lock | 0 .scannerwork/report-task.txt | 6 ------ scripts/scan.sh | 5 +++++ 4 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 .scannerwork/.sonar_lock delete mode 100644 .scannerwork/report-task.txt create mode 100644 scripts/scan.sh diff --git a/.gitignore b/.gitignore index 7be55e2..5fa2e97 100755 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ _ide_helper.php Homestead.json Homestead.yaml Thumbs.db +/.scannerwork/ diff --git a/.scannerwork/.sonar_lock b/.scannerwork/.sonar_lock deleted file mode 100644 index e69de29..0000000 diff --git a/.scannerwork/report-task.txt b/.scannerwork/report-task.txt deleted file mode 100644 index e327be9..0000000 --- a/.scannerwork/report-task.txt +++ /dev/null @@ -1,6 +0,0 @@ -projectKey=sonar_desk -serverUrl=https://sonarcube.absdev.net -serverVersion=26.9.0.129388 -dashboardUrl=https://sonarcube.absdev.net/dashboard?id=sonar_desk -ceTaskId=ed0bb599-656b-42f0-a0ec-c838a1ab98b2 -ceTaskUrl=https://sonarcube.absdev.net/api/ce/task?id=ed0bb599-656b-42f0-a0ec-c838a1ab98b2 diff --git a/scripts/scan.sh b/scripts/scan.sh new file mode 100644 index 0000000..e708714 --- /dev/null +++ b/scripts/scan.sh @@ -0,0 +1,5 @@ +sonar-scanner \ + -Dsonar.projectKey=SonarDesk \ + -Dsonar.sources=. \ + -Dsonar.host.url=https://sonarcube.absdev.net \ + -Dsonar.token=sqp_b215d121d857a0014bdad688bc76f31986a882c5 From a8eff45f493c8cfb78ddbabd4bc3348b61b3ec8c Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Thu, 1 Oct 2026 11:15:55 +0100 Subject: [PATCH 3/3] SEO additions --- Http/Controllers/Admin/CmsPageController.php | 45 ++++++++++++++ .../Admin/SeoSettingsController.php | 50 ++++++++++++++++ Http/Controllers/SeoController.php | 53 ++++++++++++++++ Models/CmsPage.php | 22 +++++++ Models/SiteSetting.php | 32 ++++++++++ .../Controllers/Admin/CmsPageController.php | 10 ++++ .../Admin/SeoSettingsController.php | 50 ++++++++++++++++ app/Http/Controllers/SeoController.php | 53 ++++++++++++++++ app/Models/CmsPage.php | 11 +++- app/Models/SiteSetting.php | 6 ++ ...0000_add_professional_seo_to_cms_pages.php | 35 +++++++++++ resources/views/admin/cms/form.blade.php | 12 +++- resources/views/admin/index.blade.php | 2 +- resources/views/admin/seo/edit.blade.php | 39 ++++++++++++ resources/views/cms/show.blade.php | 12 +++- resources/views/layouts/app.blade.php | 60 +++++++++++++------ resources/views/seo/sitemap.blade.php | 18 ++++++ routes/web.php | 7 +++ 18 files changed, 493 insertions(+), 24 deletions(-) create mode 100644 Http/Controllers/Admin/CmsPageController.php create mode 100644 Http/Controllers/Admin/SeoSettingsController.php create mode 100644 Http/Controllers/SeoController.php create mode 100644 Models/CmsPage.php create mode 100644 Models/SiteSetting.php create mode 100644 app/Http/Controllers/Admin/SeoSettingsController.php create mode 100644 app/Http/Controllers/SeoController.php create mode 100644 database/migrations/2026_10_01_100000_add_professional_seo_to_cms_pages.php create mode 100644 resources/views/admin/seo/edit.blade.php create mode 100644 resources/views/seo/sitemap.blade.php diff --git a/Http/Controllers/Admin/CmsPageController.php b/Http/Controllers/Admin/CmsPageController.php new file mode 100644 index 0000000..e12651d --- /dev/null +++ b/Http/Controllers/Admin/CmsPageController.php @@ -0,0 +1,45 @@ + CmsPage::latest()->paginate(25)]); } + public function create() { return view('admin.cms.form', ['page' => new CmsPage()]); } + public function store(Request $request) { $page = CmsPage::create($this->validated($request)); return redirect()->route('admin.cms.edit', $page)->with('status', 'Page created.'); } + public function edit(CmsPage $page) { return view('admin.cms.form', compact('page')); } + public function update(Request $request, CmsPage $page) { $page->update($this->validated($request, $page)); return back()->with('status', 'Page updated.'); } + public function destroy(CmsPage $page) { $page->delete(); return redirect()->route('admin.cms.index')->with('status', 'Page deleted.'); } + + private function validated(Request $request, ?CmsPage $page = null): array + { + $request->merge(['slug' => Str::slug($request->input('slug') ?: $request->input('title', ''))]); + $data = $request->validate([ + 'title' => ['required','string','max:160'], + 'slug' => ['required','string','max:180', Rule::unique('cms_pages','slug')->ignore($page?->id)], + 'excerpt' => ['nullable','string','max:1000'], + 'body' => ['required','string'], + 'meta_title' => ['nullable','string','max:160'], + 'meta_description' => ['nullable','string','max:320'], + 'meta_keywords' => ['nullable','string','max:500'], + 'canonical_url' => ['nullable','url:http,https','max:2048'], + 'og_title' => ['nullable','string','max:160'], + 'og_description' => ['nullable','string','max:320'], + 'og_image' => ['nullable','url:http,https','max:2048'], + 'sitemap_priority' => ['required','numeric','min:0','max:1'], + 'sitemap_change_frequency' => ['required', Rule::in(['always','hourly','daily','weekly','monthly','yearly','never'])], + ]); + $data['robots_index'] = $request->boolean('robots_index'); + $data['robots_follow'] = $request->boolean('robots_follow'); + $data['include_in_sitemap'] = $request->boolean('include_in_sitemap'); + $data['is_published'] = $request->boolean('is_published'); + $data['published_at'] = $data['is_published'] ? ($page?->published_at ?? now()) : null; + return $data; + } +} diff --git a/Http/Controllers/Admin/SeoSettingsController.php b/Http/Controllers/Admin/SeoSettingsController.php new file mode 100644 index 0000000..a7f920d --- /dev/null +++ b/Http/Controllers/Admin/SeoSettingsController.php @@ -0,0 +1,50 @@ + collect(self::KEYS)->mapWithKeys(fn (string $key) => [$key => SiteSetting::valueFor($key, '')]), + 'robotsEnabled' => SiteSetting::booleanFor('seo_robots_enabled', true), + ]); + } + + public function update(Request $request): RedirectResponse + { + $data = $request->validate([ + 'seo_site_name' => ['required', 'string', 'max:80'], + 'seo_title_suffix' => ['nullable', 'string', 'max:80'], + 'seo_default_title' => ['required', 'string', 'max:160'], + 'seo_default_description' => ['required', 'string', 'max:320'], + 'seo_default_keywords' => ['nullable', 'string', 'max:500'], + 'seo_canonical_base_url' => ['nullable', 'url:http,https', 'max:2048'], + 'seo_default_og_image' => ['nullable', 'url:http,https', 'max:2048'], + 'seo_twitter_site' => ['nullable', 'string', 'max:40', 'regex:/^@?[A-Za-z0-9_]{1,15}$/'], + 'seo_organization_name' => ['nullable', 'string', 'max:120'], + 'seo_organization_logo' => ['nullable', 'url:http,https', 'max:2048'], + 'seo_google_site_verification' => ['nullable', 'string', 'max:255'], + ]); + + foreach (self::KEYS as $key) { + SiteSetting::put($key, isset($data[$key]) ? trim((string) $data[$key]) : null); + } + SiteSetting::put('seo_robots_enabled', $request->boolean('seo_robots_enabled') ? '1' : '0'); + + return back()->with('status', 'SEO settings updated.'); + } +} diff --git a/Http/Controllers/SeoController.php b/Http/Controllers/SeoController.php new file mode 100644 index 0000000..59603f3 --- /dev/null +++ b/Http/Controllers/SeoController.php @@ -0,0 +1,53 @@ + 'home', 'priority' => '1.0', 'frequency' => 'weekly'], + ['route' => 'features', 'priority' => '0.9', 'frequency' => 'monthly'], + ['route' => 'buy', 'priority' => '0.9', 'frequency' => 'monthly'], + ['route' => 'release', 'priority' => '0.8', 'frequency' => 'weekly'], + ['route' => 'support', 'priority' => '0.6', 'frequency' => 'monthly'], + ['route' => 'privacy', 'priority' => '0.3', 'frequency' => 'yearly'], + ['route' => 'terms', 'priority' => '0.3', 'frequency' => 'yearly'], + ['route' => 'refund', 'priority' => '0.3', 'frequency' => 'yearly'], + ['route' => 'cookies', 'priority' => '0.3', 'frequency' => 'yearly'], + ])->filter(fn (array $item) => Route::has($item['route']))->values(); + + $pages = CmsPage::query() + ->where('is_published', true) + ->where('include_in_sitemap', true) + ->where('robots_index', true) + ->where(fn ($query) => $query->whereNull('published_at')->orWhere('published_at', '<=', now())) + ->orderBy('slug') + ->get(); + + return response() + ->view('seo.sitemap', compact('static', 'pages')) + ->header('Content-Type', 'application/xml; charset=UTF-8') + ->header('Cache-Control', 'public, max-age=3600'); + } + + public function robots(): Response + { + $indexable = app()->environment('production') && SiteSetting::booleanFor('seo_robots_enabled', true); + + $body = $indexable + ? "User-agent: *\nAllow: /\nDisallow: /admin/\nDisallow: /portal/\nDisallow: /login\nDisallow: /register\n\nSitemap: ".route('sitemap')."\n" + : "User-agent: *\nDisallow: /\n"; + + return response($body, 200, [ + 'Content-Type' => 'text/plain; charset=UTF-8', + 'Cache-Control' => 'public, max-age=3600', + ]); + } +} diff --git a/Models/CmsPage.php b/Models/CmsPage.php new file mode 100644 index 0000000..3209346 --- /dev/null +++ b/Models/CmsPage.php @@ -0,0 +1,22 @@ + 'boolean', 'published_at' => 'datetime', 'robots_index' => 'boolean', + 'robots_follow' => 'boolean', 'include_in_sitemap' => 'boolean', 'sitemap_priority' => 'decimal:1', + ]; + } +} diff --git a/Models/SiteSetting.php b/Models/SiteSetting.php new file mode 100644 index 0000000..872204e --- /dev/null +++ b/Models/SiteSetting.php @@ -0,0 +1,32 @@ + static::query()->find($key)?->value ?? $default); + } + + public static function booleanFor(string $key, bool $default = false): bool + { + $value = static::valueFor($key); + return $value === null ? $default : filter_var($value, FILTER_VALIDATE_BOOLEAN); + } + + public static function put(string $key, ?string $value): void + { + static::query()->updateOrCreate(['key' => $key], ['value' => $value]); + Cache::forget('sonardesk.setting.'.$key); + } +} diff --git a/app/Http/Controllers/Admin/CmsPageController.php b/app/Http/Controllers/Admin/CmsPageController.php index b0d53ab..e12651d 100644 --- a/app/Http/Controllers/Admin/CmsPageController.php +++ b/app/Http/Controllers/Admin/CmsPageController.php @@ -27,7 +27,17 @@ final class CmsPageController extends Controller 'body' => ['required','string'], 'meta_title' => ['nullable','string','max:160'], 'meta_description' => ['nullable','string','max:320'], + 'meta_keywords' => ['nullable','string','max:500'], + 'canonical_url' => ['nullable','url:http,https','max:2048'], + 'og_title' => ['nullable','string','max:160'], + 'og_description' => ['nullable','string','max:320'], + 'og_image' => ['nullable','url:http,https','max:2048'], + 'sitemap_priority' => ['required','numeric','min:0','max:1'], + 'sitemap_change_frequency' => ['required', Rule::in(['always','hourly','daily','weekly','monthly','yearly','never'])], ]); + $data['robots_index'] = $request->boolean('robots_index'); + $data['robots_follow'] = $request->boolean('robots_follow'); + $data['include_in_sitemap'] = $request->boolean('include_in_sitemap'); $data['is_published'] = $request->boolean('is_published'); $data['published_at'] = $data['is_published'] ? ($page?->published_at ?? now()) : null; return $data; diff --git a/app/Http/Controllers/Admin/SeoSettingsController.php b/app/Http/Controllers/Admin/SeoSettingsController.php new file mode 100644 index 0000000..a7f920d --- /dev/null +++ b/app/Http/Controllers/Admin/SeoSettingsController.php @@ -0,0 +1,50 @@ + collect(self::KEYS)->mapWithKeys(fn (string $key) => [$key => SiteSetting::valueFor($key, '')]), + 'robotsEnabled' => SiteSetting::booleanFor('seo_robots_enabled', true), + ]); + } + + public function update(Request $request): RedirectResponse + { + $data = $request->validate([ + 'seo_site_name' => ['required', 'string', 'max:80'], + 'seo_title_suffix' => ['nullable', 'string', 'max:80'], + 'seo_default_title' => ['required', 'string', 'max:160'], + 'seo_default_description' => ['required', 'string', 'max:320'], + 'seo_default_keywords' => ['nullable', 'string', 'max:500'], + 'seo_canonical_base_url' => ['nullable', 'url:http,https', 'max:2048'], + 'seo_default_og_image' => ['nullable', 'url:http,https', 'max:2048'], + 'seo_twitter_site' => ['nullable', 'string', 'max:40', 'regex:/^@?[A-Za-z0-9_]{1,15}$/'], + 'seo_organization_name' => ['nullable', 'string', 'max:120'], + 'seo_organization_logo' => ['nullable', 'url:http,https', 'max:2048'], + 'seo_google_site_verification' => ['nullable', 'string', 'max:255'], + ]); + + foreach (self::KEYS as $key) { + SiteSetting::put($key, isset($data[$key]) ? trim((string) $data[$key]) : null); + } + SiteSetting::put('seo_robots_enabled', $request->boolean('seo_robots_enabled') ? '1' : '0'); + + return back()->with('status', 'SEO settings updated.'); + } +} diff --git a/app/Http/Controllers/SeoController.php b/app/Http/Controllers/SeoController.php new file mode 100644 index 0000000..59603f3 --- /dev/null +++ b/app/Http/Controllers/SeoController.php @@ -0,0 +1,53 @@ + 'home', 'priority' => '1.0', 'frequency' => 'weekly'], + ['route' => 'features', 'priority' => '0.9', 'frequency' => 'monthly'], + ['route' => 'buy', 'priority' => '0.9', 'frequency' => 'monthly'], + ['route' => 'release', 'priority' => '0.8', 'frequency' => 'weekly'], + ['route' => 'support', 'priority' => '0.6', 'frequency' => 'monthly'], + ['route' => 'privacy', 'priority' => '0.3', 'frequency' => 'yearly'], + ['route' => 'terms', 'priority' => '0.3', 'frequency' => 'yearly'], + ['route' => 'refund', 'priority' => '0.3', 'frequency' => 'yearly'], + ['route' => 'cookies', 'priority' => '0.3', 'frequency' => 'yearly'], + ])->filter(fn (array $item) => Route::has($item['route']))->values(); + + $pages = CmsPage::query() + ->where('is_published', true) + ->where('include_in_sitemap', true) + ->where('robots_index', true) + ->where(fn ($query) => $query->whereNull('published_at')->orWhere('published_at', '<=', now())) + ->orderBy('slug') + ->get(); + + return response() + ->view('seo.sitemap', compact('static', 'pages')) + ->header('Content-Type', 'application/xml; charset=UTF-8') + ->header('Cache-Control', 'public, max-age=3600'); + } + + public function robots(): Response + { + $indexable = app()->environment('production') && SiteSetting::booleanFor('seo_robots_enabled', true); + + $body = $indexable + ? "User-agent: *\nAllow: /\nDisallow: /admin/\nDisallow: /portal/\nDisallow: /login\nDisallow: /register\n\nSitemap: ".route('sitemap')."\n" + : "User-agent: *\nDisallow: /\n"; + + return response($body, 200, [ + 'Content-Type' => 'text/plain; charset=UTF-8', + 'Cache-Control' => 'public, max-age=3600', + ]); + } +} diff --git a/app/Models/CmsPage.php b/app/Models/CmsPage.php index 180a18e..3209346 100644 --- a/app/Models/CmsPage.php +++ b/app/Models/CmsPage.php @@ -6,10 +6,17 @@ use Illuminate\Database\Eloquent\Model; final class CmsPage extends Model { - protected $fillable = ['title', 'slug', 'excerpt', 'body', 'meta_title', 'meta_description', 'is_published', 'published_at']; + protected $fillable = [ + 'title', 'slug', 'excerpt', 'body', 'meta_title', 'meta_description', 'meta_keywords', + 'canonical_url', 'og_title', 'og_description', 'og_image', 'robots_index', 'robots_follow', + 'include_in_sitemap', 'sitemap_priority', 'sitemap_change_frequency', 'is_published', 'published_at', + ]; protected function casts(): array { - return ['is_published' => 'boolean', 'published_at' => 'datetime']; + return [ + 'is_published' => 'boolean', 'published_at' => 'datetime', 'robots_index' => 'boolean', + 'robots_follow' => 'boolean', 'include_in_sitemap' => 'boolean', 'sitemap_priority' => 'decimal:1', + ]; } } diff --git a/app/Models/SiteSetting.php b/app/Models/SiteSetting.php index 83af974..872204e 100644 --- a/app/Models/SiteSetting.php +++ b/app/Models/SiteSetting.php @@ -18,6 +18,12 @@ final class SiteSetting extends Model return Cache::remember('sonardesk.setting.'.$key, 300, fn () => static::query()->find($key)?->value ?? $default); } + public static function booleanFor(string $key, bool $default = false): bool + { + $value = static::valueFor($key); + return $value === null ? $default : filter_var($value, FILTER_VALIDATE_BOOLEAN); + } + public static function put(string $key, ?string $value): void { static::query()->updateOrCreate(['key' => $key], ['value' => $value]); diff --git a/database/migrations/2026_10_01_100000_add_professional_seo_to_cms_pages.php b/database/migrations/2026_10_01_100000_add_professional_seo_to_cms_pages.php new file mode 100644 index 0000000..5633841 --- /dev/null +++ b/database/migrations/2026_10_01_100000_add_professional_seo_to_cms_pages.php @@ -0,0 +1,35 @@ +string('meta_keywords', 500)->nullable()->after('meta_description'); + $table->string('canonical_url', 2048)->nullable()->after('meta_keywords'); + $table->string('og_title')->nullable()->after('canonical_url'); + $table->string('og_description', 320)->nullable()->after('og_title'); + $table->string('og_image', 2048)->nullable()->after('og_description'); + $table->boolean('robots_index')->default(true)->after('og_image'); + $table->boolean('robots_follow')->default(true)->after('robots_index'); + $table->boolean('include_in_sitemap')->default(true)->after('robots_follow')->index(); + $table->decimal('sitemap_priority', 2, 1)->default(0.5)->after('include_in_sitemap'); + $table->string('sitemap_change_frequency', 20)->default('monthly')->after('sitemap_priority'); + }); + } + + public function down(): void + { + Schema::table('cms_pages', function (Blueprint $table): void { + $table->dropIndex(['include_in_sitemap']); + $table->dropColumn([ + 'meta_keywords', 'canonical_url', 'og_title', 'og_description', 'og_image', + 'robots_index', 'robots_follow', 'include_in_sitemap', 'sitemap_priority', + 'sitemap_change_frequency', + ]); + }); + } +}; diff --git a/resources/views/admin/cms/form.blade.php b/resources/views/admin/cms/form.blade.php index 2ba7d36..9df446c 100644 --- a/resources/views/admin/cms/form.blade.php +++ b/resources/views/admin/cms/form.blade.php @@ -6,7 +6,17 @@ -
+
+
Search appearance

Page SEO

Override global defaults only where this page needs specific search or social metadata.

Global SEO settings
+
+ + +
+ +
+
+ +
@if($page->exists && $page->is_published)View page@endif
diff --git a/resources/views/admin/index.blade.php b/resources/views/admin/index.blade.php index f4198c6..e4d91e4 100755 --- a/resources/views/admin/index.blade.php +++ b/resources/views/admin/index.blade.php @@ -2,5 +2,5 @@ @section('content')
Admin

Licensing administration

Manage licences, activations and customer support.

{{ $licenseCount }}

Licences

{{ $activeCount }}

Not revoked

{{ $activationCount }}

Active installations

{{ $userCount }}

Portal users

-
Manage releasesManage licencesManage contentSupport inbox
+
Manage releasesManage licencesManage contentSEO & searchSupport inbox
@endsection diff --git a/resources/views/admin/seo/edit.blade.php b/resources/views/admin/seo/edit.blade.php new file mode 100644 index 0000000..67fcb35 --- /dev/null +++ b/resources/views/admin/seo/edit.blade.php @@ -0,0 +1,39 @@ +@extends('layouts.app') +@section('content') +
+
Search & discovery

SEO settings

Control how SonarDesk appears in search engines and when pages are shared.

+ +
+
@csrf @method('PUT') +
+
Search appearance

Site defaults

Fallback metadata used whenever a page does not provide its own SEO values.

Global
+
+ + +
+ + + + +
+ +
+
Social sharing

Open Graph & social

Professional defaults for link previews across social networks and messaging applications.

+ +
+ +
+ +
+
Google Search Console

Domain verification

Use a Domain property in Google Search Console. Google will provide a TXT value that you add at your DNS provider.

DNS TXT
+
Recommended verification methodAdd Google’s google-site-verification=… TXT record to the root domain. DNS verification covers all protocols and subdomains.
+ +
+ +
+
Crawler control

Indexing

Production crawler controls. Non-production environments remain blocked automatically.

+ +
+
Search configurationChanges apply to public metadata immediately.
+
+@endsection diff --git a/resources/views/cms/show.blade.php b/resources/views/cms/show.blade.php index 909e3cf..35da777 100644 --- a/resources/views/cms/show.blade.php +++ b/resources/views/cms/show.blade.php @@ -1,4 +1,14 @@ -@extends('layouts.app', ['title' => $page->meta_title ?: $page->title.' — SonarDesk', 'description' => $page->meta_description ?: $page->excerpt]) +@extends('layouts.app', [ + 'title' => $page->meta_title ?: $page->title.' — '.\App\Models\SiteSetting::valueFor('seo_site_name', 'SonarDesk'), + 'description' => $page->meta_description ?: $page->excerpt, + 'keywords' => $page->meta_keywords, + 'canonical' => $page->canonical_url ?: null, + 'ogTitle' => $page->og_title ?: null, + 'ogDescription' => $page->og_description ?: null, + 'ogImage' => $page->og_image ?: null, + 'robotsIndex' => $page->robots_index, + 'robotsFollow' => $page->robots_follow, +]) @section('content') @endsection diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 4dbe278..7bd879d 100755 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -6,26 +6,45 @@ @php - $pageTitle = $title ?? 'SonarDesk'; - $pageDescription = $description ?? 'Native SonarQube monitoring for macOS. Track projects, quality gates, bugs, vulnerabilities and code smells from one professional desktop app.'; - $canonicalUrl = url()->current(); - $socialImage = url('/icons/og-sonardesk.png'); + $seoSiteName = \App\Models\SiteSetting::valueFor('seo_site_name', 'SonarDesk'); + $seoDefaultTitle = \App\Models\SiteSetting::valueFor('seo_default_title', 'SonarDesk — Native SonarQube Monitoring for macOS'); + $seoDefaultDescription = \App\Models\SiteSetting::valueFor('seo_default_description', 'Native SonarQube monitoring for macOS. Track projects, quality gates, bugs, vulnerabilities and code smells from one professional desktop app.'); + $pageTitle = $title ?? $seoDefaultTitle; + $pageDescription = $description ?? $seoDefaultDescription; + $pageKeywords = $keywords ?? \App\Models\SiteSetting::valueFor('seo_default_keywords'); + $canonicalBase = rtrim(\App\Models\SiteSetting::valueFor('seo_canonical_base_url', config('app.url')), '/'); + $canonicalUrl = $canonical ?? ($canonicalBase . request()->getPathInfo()); + $socialImage = $ogImage ?? \App\Models\SiteSetting::valueFor('seo_default_og_image', url('/icons/og-sonardesk.png')); + $ogTitle = $ogTitle ?? $pageTitle; + $ogDescription = $ogDescription ?? $pageDescription; + $robotsIndex = $robotsIndex ?? true; + $robotsFollow = $robotsFollow ?? true; + $productionIndexing = app()->environment('production') && \App\Models\SiteSetting::booleanFor('seo_robots_enabled', true); + $robots = $productionIndexing ? (($robotsIndex ? 'index' : 'noindex').','.($robotsFollow ? 'follow' : 'nofollow').',max-image-preview:large,max-snippet:-1,max-video-preview:-1') : 'noindex,nofollow'; + $twitterSite = \App\Models\SiteSetting::valueFor('seo_twitter_site'); + $googleVerification = \App\Models\SiteSetting::valueFor('seo_google_site_verification'); + $organizationName = \App\Models\SiteSetting::valueFor('seo_organization_name', $seoSiteName); + $organizationLogo = \App\Models\SiteSetting::valueFor('seo_organization_logo', url('/icons/sonardesk-icon-512.png')); + $schema = [ + '@context' => 'https://schema.org', '@type' => 'SoftwareApplication', 'name' => $seoSiteName, + 'applicationCategory' => 'DeveloperApplication', 'operatingSystem' => 'macOS', 'url' => route('home'), + 'description' => $seoDefaultDescription, 'publisher' => ['@type' => 'Organization', 'name' => $organizationName, 'logo' => ['@type' => 'ImageObject', 'url' => $organizationLogo]], + ]; @endphp {{ $pageTitle }} + @if($pageKeywords)@endif + + + @if($googleVerification)@endif - - + + - @if(app()->environment('production')) - - @else - - @endif @@ -36,17 +55,18 @@ - - - + + + - - - - - + + + @if($twitterSite)@endif + + + @stack('head') diff --git a/resources/views/seo/sitemap.blade.php b/resources/views/seo/sitemap.blade.php new file mode 100644 index 0000000..517eb1c --- /dev/null +++ b/resources/views/seo/sitemap.blade.php @@ -0,0 +1,18 @@ +{!! '' !!} + +@foreach($static as $item) + + {{ route($item['route']) }} + {{ $item['frequency'] }} + {{ $item['priority'] }} + +@endforeach +@foreach($pages as $page) + + {{ route('cms.show', $page->slug) }} + {{ $page->updated_at->toAtomString() }} + {{ $page->sitemap_change_frequency }} + {{ number_format((float) $page->sitemap_priority, 1, '.', '') }} + +@endforeach + diff --git a/routes/web.php b/routes/web.php index 0438c31..cc02748 100755 --- a/routes/web.php +++ b/routes/web.php @@ -4,6 +4,8 @@ use App\Http\Controllers\Admin\AdminController; use App\Http\Controllers\Admin\SupportTicketController; use App\Http\Controllers\Admin\ReleaseController as AdminReleaseController; use App\Http\Controllers\Admin\CmsPageController as AdminCmsPageController; +use App\Http\Controllers\Admin\SeoSettingsController; +use App\Http\Controllers\SeoController; use App\Http\Controllers\CmsPageController; use App\Http\Controllers\Auth\AuthenticatedSessionController; use App\Http\Controllers\Auth\EmailVerificationController; @@ -16,6 +18,9 @@ use App\Http\Controllers\Support\SupportController; use App\Http\Middleware\AdminOnly; use Illuminate\Support\Facades\Route; +Route::get('/sitemap.xml', [SeoController::class, 'sitemap'])->name('sitemap'); +Route::get('/robots.txt', [SeoController::class, 'robots'])->name('robots'); + Route::get('/', fn () => view('portal.welcome', ['latestRelease' => \App\Models\SonarDeskRelease::published()->where('channel', 'stable')->latest('published_at')->first()]))->name('home'); Route::get('/buy', CheckoutController::class)->name('buy'); Route::view('/features', 'features')->name('features'); @@ -59,6 +64,8 @@ Route::middleware(['auth', 'verified', AdminOnly::class])->prefix('admin')->name Route::get('/licences/{license}', [AdminController::class, 'show'])->name('license'); Route::post('/licences/{license}/revoke', [AdminController::class, 'revoke'])->name('revoke'); Route::post('/licences/{license}/restore', [AdminController::class, 'restore'])->name('restore'); + Route::get('/seo', [SeoSettingsController::class, 'edit'])->name('seo.edit'); + Route::put('/seo', [SeoSettingsController::class, 'update'])->name('seo.update'); Route::get('/cms', [AdminCmsPageController::class, 'index'])->name('cms.index'); Route::get('/cms/create', [AdminCmsPageController::class, 'create'])->name('cms.create'); Route::post('/cms', [AdminCmsPageController::class, 'store'])->name('cms.store');