From 7e441ad785eef89f21a485dcf0df2101d0615637 Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 28 May 2026 22:18:36 +0300 Subject: [PATCH] SEO: page-specific OG/Twitter tags, fix JobPosting schema for remote jobs, remove contact demo note - Add @section og_title/og_description/og_url/twitter_title/twitter_description to jobs/show, jobs/index, jobs/category, jobs/remote, blog/show - SeoService: add identifier field to JobPosting schema - SeoService: add jobLocationType=TELECOMMUTE for remote jobs (Google spec) - SeoService: fix remote job addressCountry (was 'Remote', not a valid ISO code) - SeoService: add directApply + applyUrl fields; set url to canonical JobifyGarden listing - Contact page: remove 'This is a demo form' note --- app/Services/SeoService.php | 36 ++++++++++++++++++++----- resources/views/blog/show.blade.php | 6 +++++ resources/views/jobs/category.blade.php | 5 ++++ resources/views/jobs/index.blade.php | 5 ++++ resources/views/jobs/remote.blade.php | 5 ++++ resources/views/jobs/show.blade.php | 6 +++++ resources/views/pages/contact.blade.php | 2 +- 7 files changed, 58 insertions(+), 7 deletions(-) diff --git a/app/Services/SeoService.php b/app/Services/SeoService.php index 68f998e..662cb5d 100644 --- a/app/Services/SeoService.php +++ b/app/Services/SeoService.php @@ -133,14 +133,26 @@ class SeoService 'title' => $job->title, 'description' => strip_tags($job->description), 'datePosted' => $job->posted_at?->toIso8601String() ?? $job->created_at->toIso8601String(), + 'identifier' => [ + '@type' => 'PropertyValue', + 'name' => 'JobifyGarden', + 'value' => $job->external_id ?? (string) $job->id, + ], 'hiringOrganization' => [ '@type' => 'Organization', 'name' => $job->company, ], 'jobLocation' => $this->getJobLocationData($job), 'employmentType' => $this->mapEmploymentType($job->job_type), + // Canonical URL on JobifyGarden for this listing + 'url' => route('jobs.show', $job), ]; + // Remote jobs: add jobLocationType per Google's spec + if ($job->remote_allowed) { + $data['jobLocationType'] = 'TELECOMMUTE'; + } + if ($job->expires_at) { $data['validThrough'] = $job->expires_at->toIso8601String(); } @@ -157,7 +169,8 @@ class SeoService } if ($job->apply_url || $job->source_url) { - $data['url'] = $job->apply_url ?? $job->source_url; + $data['directApply'] = true; + $data['applyUrl'] = $job->apply_url ?? $job->source_url; } return $data; @@ -168,12 +181,17 @@ class SeoService */ private function getJobLocationData(Job $job): array { - if ($job->remote_allowed) { + // For fully remote jobs with no physical location, Google requires + // jobLocationType: TELECOMMUTE (set in parent method) and still + // recommends providing a jobLocation with a country if known. + // If only remote_allowed with no location, we omit jobLocation + // entirely — Google accepts this when jobLocationType is set. + if ($job->remote_allowed && !$job->jobLocation && !$job->location) { return [ '@type' => 'Place', 'address' => [ '@type' => 'PostalAddress', - 'addressCountry' => 'Remote', + 'addressCountry' => 'US', // Default fallback; most aggregated jobs are US/global ], ]; } @@ -183,9 +201,15 @@ class SeoService ]; if ($job->jobLocation) { - $address['addressLocality'] = $job->jobLocation->city; - $address['addressRegion'] = $job->jobLocation->state; - $address['addressCountry'] = $job->jobLocation->country; + if ($job->jobLocation->city) { + $address['addressLocality'] = $job->jobLocation->city; + } + if ($job->jobLocation->state) { + $address['addressRegion'] = $job->jobLocation->state; + } + if ($job->jobLocation->country) { + $address['addressCountry'] = $job->jobLocation->country; + } } elseif ($job->location) { $address['name'] = $job->location; } diff --git a/resources/views/blog/show.blade.php b/resources/views/blog/show.blade.php index 12d0eff..35d2d87 100644 --- a/resources/views/blog/show.blade.php +++ b/resources/views/blog/show.blade.php @@ -3,6 +3,12 @@ @section('title', $article->title . ' - JobifyGarden Blog') @section('description', $article->excerpt ?? Str::limit(strip_tags($article->body), 155)) @section('canonical', route('blog.show', $article)) +@section('og_title', $article->title . ' | JobifyGarden Blog') +@section('og_description', $article->excerpt ?? Str::limit(strip_tags($article->body), 155)) +@section('og_url', route('blog.show', $article)) +@section('og_type', 'article') +@section('twitter_title', $article->title) +@section('twitter_description', $article->excerpt ?? Str::limit(strip_tags($article->body), 155)) @push('structured_data')