286 lines
8.8 KiB
PHP
286 lines
8.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Job;
|
|
use App\Models\JobCategory;
|
|
use Illuminate\Support\Str;
|
|
|
|
class SeoService
|
|
{
|
|
/**
|
|
* Generate meta tags for a job
|
|
*/
|
|
public function getJobMetaTags(Job $job): array
|
|
{
|
|
$title = $job->title . ' at ' . $job->company . ' - JobifyGarden';
|
|
$description = Str::limit(strip_tags($job->description), 155);
|
|
$url = route('jobs.show', $job->id);
|
|
|
|
return [
|
|
'title' => $title,
|
|
'description' => $description,
|
|
'keywords' => $this->generateJobKeywords($job),
|
|
'canonical' => $url,
|
|
'og_title' => $job->title . ' at ' . $job->company,
|
|
'og_description' => $description,
|
|
'og_url' => $url,
|
|
'og_type' => 'website',
|
|
'og_image' => asset('images/og-default.jpg'),
|
|
'twitter_card' => 'summary_large_image',
|
|
'twitter_title' => $job->title . ' at ' . $job->company,
|
|
'twitter_description' => $description,
|
|
'twitter_image' => asset('images/twitter-card-default.jpg'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate meta tags for category page
|
|
*/
|
|
public function getCategoryMetaTags(JobCategory $category, int $jobCount): array
|
|
{
|
|
$title = $category->name . ' Jobs - JobifyGarden';
|
|
$description = "Browse {$jobCount} {$category->name} job opportunities. " . ($category->description ?? 'Find your next career in ' . $category->name . '.');
|
|
$url = route('jobs.category', $category->slug);
|
|
|
|
return [
|
|
'title' => $title,
|
|
'description' => Str::limit($description, 155),
|
|
'keywords' => $this->generateCategoryKeywords($category),
|
|
'canonical' => $url,
|
|
'og_title' => $category->name . ' Jobs',
|
|
'og_description' => Str::limit($description, 155),
|
|
'og_url' => $url,
|
|
'og_type' => 'website',
|
|
'og_image' => asset('images/og-default.jpg'),
|
|
'twitter_card' => 'summary',
|
|
'twitter_title' => $category->name . ' Jobs',
|
|
'twitter_description' => Str::limit($description, 155),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate meta tags for homepage
|
|
*/
|
|
public function getHomeMetaTags(int $jobCount): array
|
|
{
|
|
return [
|
|
'title' => 'JobifyGarden - Find Your Dream Job | ' . number_format($jobCount) . '+ Jobs',
|
|
'description' => 'Discover thousands of job opportunities from multiple sources. Search remote jobs, full-time positions, and more. Your next career starts here.',
|
|
'keywords' => 'jobs, careers, employment, job search, remote jobs, full-time jobs, part-time jobs, find jobs',
|
|
'canonical' => route('home'),
|
|
'og_title' => 'JobifyGarden - Find Your Dream Job',
|
|
'og_description' => 'Discover thousands of job opportunities from multiple sources. Your next career starts here.',
|
|
'og_url' => route('home'),
|
|
'og_type' => 'website',
|
|
'og_image' => asset('images/og-home.jpg'),
|
|
'twitter_card' => 'summary_large_image',
|
|
'twitter_title' => 'JobifyGarden - Find Your Dream Job',
|
|
'twitter_description' => 'Discover thousands of job opportunities from multiple sources.',
|
|
'twitter_image' => asset('images/twitter-card-home.jpg'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate keywords for a job
|
|
*/
|
|
private function generateJobKeywords(Job $job): string
|
|
{
|
|
$keywords = [
|
|
$job->title,
|
|
$job->company,
|
|
$job->job_type,
|
|
$job->experience_level,
|
|
];
|
|
|
|
if ($job->category) {
|
|
$keywords[] = $job->category->name;
|
|
}
|
|
|
|
if ($job->location) {
|
|
$keywords[] = $job->location;
|
|
}
|
|
|
|
if ($job->remote_allowed) {
|
|
$keywords[] = 'remote';
|
|
$keywords[] = 'work from home';
|
|
}
|
|
|
|
return implode(', ', array_filter($keywords));
|
|
}
|
|
|
|
/**
|
|
* Generate keywords for a category
|
|
*/
|
|
private function generateCategoryKeywords(JobCategory $category): string
|
|
{
|
|
return implode(', ', [
|
|
$category->name . ' jobs',
|
|
$category->name . ' careers',
|
|
$category->name . ' employment',
|
|
'find ' . $category->name . ' jobs',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Generate JSON-LD structured data for a job
|
|
*/
|
|
public function generateJobStructuredData(Job $job): array
|
|
{
|
|
$data = [
|
|
'@context' => 'https://schema.org/',
|
|
'@type' => 'JobPosting',
|
|
'title' => $job->title,
|
|
'description' => strip_tags($job->description),
|
|
'datePosted' => $job->posted_at?->toIso8601String() ?? $job->created_at->toIso8601String(),
|
|
'hiringOrganization' => [
|
|
'@type' => 'Organization',
|
|
'name' => $job->company,
|
|
],
|
|
'jobLocation' => $this->getJobLocationData($job),
|
|
'employmentType' => $this->mapEmploymentType($job->job_type),
|
|
];
|
|
|
|
if ($job->expires_at) {
|
|
$data['validThrough'] = $job->expires_at->toIso8601String();
|
|
}
|
|
|
|
if ($job->salary_min || $job->salary_max) {
|
|
$data['baseSalary'] = $this->getSalaryData($job);
|
|
}
|
|
|
|
if ($job->experience_level) {
|
|
$data['experienceRequirements'] = [
|
|
'@type' => 'OccupationalExperienceRequirements',
|
|
'monthsOfExperience' => $this->mapExperienceLevel($job->experience_level),
|
|
];
|
|
}
|
|
|
|
if ($job->apply_url || $job->source_url) {
|
|
$data['url'] = $job->apply_url ?? $job->source_url;
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Get job location data for structured data
|
|
*/
|
|
private function getJobLocationData(Job $job): array
|
|
{
|
|
if ($job->remote_allowed) {
|
|
return [
|
|
'@type' => 'Place',
|
|
'address' => [
|
|
'@type' => 'PostalAddress',
|
|
'addressCountry' => 'Remote',
|
|
],
|
|
];
|
|
}
|
|
|
|
$address = [
|
|
'@type' => 'PostalAddress',
|
|
];
|
|
|
|
if ($job->jobLocation) {
|
|
$address['addressLocality'] = $job->jobLocation->city;
|
|
$address['addressRegion'] = $job->jobLocation->state;
|
|
$address['addressCountry'] = $job->jobLocation->country;
|
|
} elseif ($job->location) {
|
|
$address['name'] = $job->location;
|
|
}
|
|
|
|
return [
|
|
'@type' => 'Place',
|
|
'address' => $address,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get salary data for structured data
|
|
*/
|
|
private function getSalaryData(Job $job): array
|
|
{
|
|
$currency = $job->salary_currency ?? 'USD';
|
|
|
|
if ($job->salary_min && $job->salary_max) {
|
|
return [
|
|
'@type' => 'MonetaryAmount',
|
|
'currency' => $currency,
|
|
'value' => [
|
|
'@type' => 'QuantitativeValue',
|
|
'minValue' => $job->salary_min,
|
|
'maxValue' => $job->salary_max,
|
|
'unitText' => 'YEAR',
|
|
],
|
|
];
|
|
}
|
|
|
|
$value = $job->salary_min ?? $job->salary_max;
|
|
|
|
return [
|
|
'@type' => 'MonetaryAmount',
|
|
'currency' => $currency,
|
|
'value' => [
|
|
'@type' => 'QuantitativeValue',
|
|
'value' => $value,
|
|
'unitText' => 'YEAR',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Map job type to schema.org employment type
|
|
*/
|
|
private function mapEmploymentType(?string $jobType): string
|
|
{
|
|
return match ($jobType) {
|
|
'full-time', 'full_time' => 'FULL_TIME',
|
|
'part-time', 'part_time' => 'PART_TIME',
|
|
'contract' => 'CONTRACTOR',
|
|
'temporary' => 'TEMPORARY',
|
|
'internship' => 'INTERN',
|
|
'freelance' => 'CONTRACTOR',
|
|
default => 'FULL_TIME',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Map experience level to months
|
|
*/
|
|
private function mapExperienceLevel(?string $level): int
|
|
{
|
|
return match ($level) {
|
|
'entry' => 0,
|
|
'mid' => 24,
|
|
'senior' => 60,
|
|
'lead' => 84,
|
|
'executive' => 120,
|
|
default => 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate breadcrumb structured data
|
|
*/
|
|
public function generateBreadcrumbStructuredData(array $items): array
|
|
{
|
|
$listItems = [];
|
|
|
|
foreach ($items as $index => $item) {
|
|
$listItems[] = [
|
|
'@type' => 'ListItem',
|
|
'position' => $index + 1,
|
|
'name' => $item['name'],
|
|
'item' => $item['url'],
|
|
];
|
|
}
|
|
|
|
return [
|
|
'@context' => 'https://schema.org',
|
|
'@type' => 'BreadcrumbList',
|
|
'itemListElement' => $listItems,
|
|
];
|
|
}
|
|
}
|