phase 9
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\CacheService;
|
||||
use App\Services\SitemapService;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class SitemapController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private SitemapService $sitemapService,
|
||||
private CacheService $cacheService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generate and serve main sitemap
|
||||
*/
|
||||
public function index(): Response
|
||||
{
|
||||
$xml = $this->cacheService->remember(
|
||||
'sitemap:main',
|
||||
3600, // 1 hour cache
|
||||
fn() => $this->sitemapService->generateSitemap()
|
||||
);
|
||||
|
||||
return response($xml, 200)
|
||||
->header('Content-Type', 'application/xml');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and serve sitemap index
|
||||
*/
|
||||
public function sitemapIndex(): Response
|
||||
{
|
||||
$xml = $this->cacheService->remember(
|
||||
'sitemap:index',
|
||||
3600,
|
||||
fn() => $this->sitemapService->generateSitemapIndex()
|
||||
);
|
||||
|
||||
return response($xml, 200)
|
||||
->header('Content-Type', 'application/xml');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and serve main sitemap (static pages)
|
||||
*/
|
||||
public function mainSitemap(): Response
|
||||
{
|
||||
$xml = $this->cacheService->remember(
|
||||
'sitemap:main-pages',
|
||||
3600,
|
||||
fn() => $this->sitemapService->generateMainSitemap()
|
||||
);
|
||||
|
||||
return response($xml, 200)
|
||||
->header('Content-Type', 'application/xml');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and serve jobs sitemap
|
||||
*/
|
||||
public function jobsSitemap(): Response
|
||||
{
|
||||
$xml = $this->cacheService->remember(
|
||||
'sitemap:jobs',
|
||||
1800, // 30 minutes cache (jobs change more frequently)
|
||||
fn() => $this->sitemapService->generateJobsSitemap()
|
||||
);
|
||||
|
||||
return response($xml, 200)
|
||||
->header('Content-Type', 'application/xml');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Job;
|
||||
use App\Models\JobCategory;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class SitemapService
|
||||
{
|
||||
/**
|
||||
* Generate XML sitemap
|
||||
*/
|
||||
public function generateSitemap(): string
|
||||
{
|
||||
$xml = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>');
|
||||
|
||||
// Add homepage
|
||||
$this->addUrl($xml, route('home'), now(), 'daily', '1.0');
|
||||
|
||||
// Add jobs index
|
||||
$this->addUrl($xml, route('jobs.index'), now(), 'daily', '0.9');
|
||||
|
||||
// Add remote jobs
|
||||
$this->addUrl($xml, route('jobs.remote'), now(), 'daily', '0.9');
|
||||
|
||||
// Add categories
|
||||
$categories = JobCategory::all();
|
||||
foreach ($categories as $category) {
|
||||
$this->addUrl(
|
||||
$xml,
|
||||
route('jobs.category', $category->slug),
|
||||
$category->updated_at,
|
||||
'daily',
|
||||
'0.8'
|
||||
);
|
||||
}
|
||||
|
||||
// Add active jobs
|
||||
$jobs = Job::where(function ($query) {
|
||||
$query->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
})
|
||||
->orderBy('posted_at', 'desc')
|
||||
->limit(10000) // Limit to 10k most recent jobs
|
||||
->get(['id', 'posted_at', 'updated_at']);
|
||||
|
||||
foreach ($jobs as $job) {
|
||||
$this->addUrl(
|
||||
$xml,
|
||||
route('jobs.show', $job->id),
|
||||
$job->updated_at ?? $job->posted_at,
|
||||
'weekly',
|
||||
'0.7'
|
||||
);
|
||||
}
|
||||
|
||||
return $xml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add URL to sitemap
|
||||
*/
|
||||
private function addUrl(
|
||||
\SimpleXMLElement $xml,
|
||||
string $url,
|
||||
?\DateTimeInterface $lastmod = null,
|
||||
string $changefreq = 'weekly',
|
||||
string $priority = '0.5'
|
||||
): void {
|
||||
$urlElement = $xml->addChild('url');
|
||||
$urlElement->addChild('loc', htmlspecialchars($url));
|
||||
|
||||
if ($lastmod) {
|
||||
$urlElement->addChild('lastmod', $lastmod->format('Y-m-d'));
|
||||
}
|
||||
|
||||
$urlElement->addChild('changefreq', $changefreq);
|
||||
$urlElement->addChild('priority', $priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate sitemap index for large sites
|
||||
*/
|
||||
public function generateSitemapIndex(): string
|
||||
{
|
||||
$xml = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></sitemapindex>');
|
||||
|
||||
// Add main sitemap
|
||||
$this->addSitemapToIndex($xml, route('sitemap.main'), now());
|
||||
|
||||
// Add jobs sitemap
|
||||
$this->addSitemapToIndex($xml, route('sitemap.jobs'), now());
|
||||
|
||||
return $xml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add sitemap to index
|
||||
*/
|
||||
private function addSitemapToIndex(\SimpleXMLElement $xml, string $url, \DateTimeInterface $lastmod): void
|
||||
{
|
||||
$sitemapElement = $xml->addChild('sitemap');
|
||||
$sitemapElement->addChild('loc', htmlspecialchars($url));
|
||||
$sitemapElement->addChild('lastmod', $lastmod->format('Y-m-d'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate main sitemap (static pages and categories)
|
||||
*/
|
||||
public function generateMainSitemap(): string
|
||||
{
|
||||
$xml = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>');
|
||||
|
||||
// Add homepage
|
||||
$this->addUrl($xml, route('home'), now(), 'daily', '1.0');
|
||||
|
||||
// Add jobs index
|
||||
$this->addUrl($xml, route('jobs.index'), now(), 'daily', '0.9');
|
||||
|
||||
// Add remote jobs
|
||||
$this->addUrl($xml, route('jobs.remote'), now(), 'daily', '0.9');
|
||||
|
||||
// Add categories
|
||||
$categories = JobCategory::all();
|
||||
foreach ($categories as $category) {
|
||||
$this->addUrl(
|
||||
$xml,
|
||||
route('jobs.category', $category->slug),
|
||||
$category->updated_at,
|
||||
'daily',
|
||||
'0.8'
|
||||
);
|
||||
}
|
||||
|
||||
return $xml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate jobs sitemap
|
||||
*/
|
||||
public function generateJobsSitemap(): string
|
||||
{
|
||||
$xml = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>');
|
||||
|
||||
// Add active jobs
|
||||
$jobs = Job::where(function ($query) {
|
||||
$query->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
})
|
||||
->orderBy('posted_at', 'desc')
|
||||
->limit(50000) // Sitemap limit
|
||||
->get(['id', 'posted_at', 'updated_at']);
|
||||
|
||||
foreach ($jobs as $job) {
|
||||
$this->addUrl(
|
||||
$xml,
|
||||
route('jobs.show', $job->id),
|
||||
$job->updated_at ?? $job->posted_at,
|
||||
'weekly',
|
||||
'0.7'
|
||||
);
|
||||
}
|
||||
|
||||
return $xml->asXML();
|
||||
}
|
||||
}
|
||||
+15
-1
@@ -1,2 +1,16 @@
|
||||
# Robots.txt for JobifyGarden
|
||||
User-agent: *
|
||||
Disallow:
|
||||
Allow: /
|
||||
Disallow: /admin/
|
||||
Disallow: /api/
|
||||
|
||||
# Google-specific
|
||||
User-agent: Googlebot
|
||||
Allow: /
|
||||
|
||||
# Sitemap location
|
||||
Sitemap: https://jobifygarden.com/sitemap.xml
|
||||
Sitemap: https://jobifygarden.com/sitemap-index.xml
|
||||
|
||||
# Crawl delay (prevents aggressive crawling)
|
||||
Crawl-delay: 1
|
||||
|
||||
@@ -1,7 +1,35 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'JobifyGarden - Find Your Dream Job')
|
||||
@section('description', 'Discover thousands of job opportunities from multiple sources. Browse remote jobs, full-time positions, and more.')
|
||||
@section('title', 'JobifyGarden - Find Your Dream Job | ' . number_format($stats['active_jobs'] ?? 0) . '+ Jobs')
|
||||
@section('description', 'Discover thousands of job opportunities from multiple sources. Browse remote jobs, full-time positions, and more. Your next career starts here.')
|
||||
@section('canonical', route('home'))
|
||||
|
||||
@push('structured_data')
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": "JobifyGarden",
|
||||
"url": "{{ route('home') }}",
|
||||
"potentialAction": {
|
||||
"@type": "SearchAction",
|
||||
"target": "{{ route('jobs.index') }}?keywords={search_term_string}",
|
||||
"query-input": "required name=search_term_string"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "JobifyGarden",
|
||||
"url": "{{ url('/') }}",
|
||||
"logo": "{{ asset('images/logo.png') }}",
|
||||
"description": "Job aggregator platform helping job seekers find opportunities from multiple sources"
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@section('main-class', '')
|
||||
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', $category->name . ' Jobs - JobifyGarden')
|
||||
@section('description', 'Browse ' . $category->name . ' job opportunities. ' . ($category->description ?? ''))
|
||||
@section('description', 'Browse ' . $jobs->total() . ' ' . $category->name . ' job opportunities. ' . ($category->description ?? 'Find your next career in ' . $category->name . '.'))
|
||||
@section('canonical', route('jobs.category', $category->slug))
|
||||
|
||||
@push('structured_data')
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode(app(\App\Services\SeoService::class)->generateBreadcrumbStructuredData([
|
||||
['name' => 'Home', 'url' => route('home')],
|
||||
['name' => 'Jobs', 'url' => route('jobs.index')],
|
||||
['name' => $category->name, 'url' => route('jobs.category', $category->slug)]
|
||||
]), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) !!}
|
||||
</script>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "CollectionPage",
|
||||
"name": "{{ $category->name }} Jobs",
|
||||
"description": "{{ 'Browse ' . $jobs->total() . ' ' . $category->name . ' job opportunities' }}",
|
||||
"url": "{{ route('jobs.category', $category->slug) }}"
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Browse Jobs - JobifyGarden')
|
||||
@section('description', 'Browse thousands of job opportunities. Filter by location, job type, experience level, and more.')
|
||||
@section('title', 'Browse Jobs - JobifyGarden | ' . number_format($jobs->total()) . ' Opportunities')
|
||||
@section('description', 'Browse ' . number_format($jobs->total()) . ' job opportunities. Filter by location, job type, experience level, and more to find your perfect role.')
|
||||
@section('canonical', route('jobs.index'))
|
||||
|
||||
@push('structured_data')
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode(app(\App\Services\SeoService::class)->generateBreadcrumbStructuredData([
|
||||
['name' => 'Home', 'url' => route('home')],
|
||||
['name' => 'Jobs', 'url' => route('jobs.index')]
|
||||
]), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) !!}
|
||||
</script>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "CollectionPage",
|
||||
"name": "Browse Jobs",
|
||||
"description": "Browse thousands of job opportunities from multiple sources",
|
||||
"url": "{{ route('jobs.index') }}"
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Remote Jobs - JobifyGarden')
|
||||
@section('description', 'Discover remote job opportunities from companies worldwide. Work from anywhere.')
|
||||
@section('title', 'Remote Jobs - JobifyGarden | ' . number_format($jobs->total()) . ' Work From Home Opportunities')
|
||||
@section('description', 'Discover ' . number_format($jobs->total()) . ' remote job opportunities from companies worldwide. Work from anywhere, find flexible positions.')
|
||||
@section('canonical', route('jobs.remote'))
|
||||
|
||||
@push('structured_data')
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode(app(\App\Services\SeoService::class)->generateBreadcrumbStructuredData([
|
||||
['name' => 'Home', 'url' => route('home')],
|
||||
['name' => 'Jobs', 'url' => route('jobs.index')],
|
||||
['name' => 'Remote Jobs', 'url' => route('jobs.remote')]
|
||||
]), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) !!}
|
||||
</script>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "CollectionPage",
|
||||
"name": "Remote Jobs",
|
||||
"description": "Work from anywhere remote job opportunities",
|
||||
"url": "{{ route('jobs.remote') }}"
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
@@ -2,6 +2,24 @@
|
||||
|
||||
@section('title', $job->title . ' at ' . $job->company . ' - JobifyGarden')
|
||||
@section('description', Str::limit(strip_tags($job->description), 155))
|
||||
@section('canonical', route('jobs.show', $job->id))
|
||||
|
||||
@push('structured_data')
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode(app(\App\Services\SeoService::class)->generateJobStructuredData($job), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) !!}
|
||||
</script>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{!! json_encode(app(\App\Services\SeoService::class)->generateBreadcrumbStructuredData([
|
||||
['name' => 'Home', 'url' => route('home')],
|
||||
['name' => 'Jobs', 'url' => route('jobs.index')],
|
||||
@if($job->category)
|
||||
['name' => $job->category->name, 'url' => route('jobs.category', $job->category->slug)],
|
||||
@endif
|
||||
['name' => $job->title, 'url' => route('jobs.show', $job->id)]
|
||||
]), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) !!}
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
@@ -7,6 +7,27 @@
|
||||
|
||||
<title>@yield('title', 'JobifyGarden - Find Your Dream Job')</title>
|
||||
<meta name="description" content="@yield('description', 'Discover thousands of job opportunities from multiple sources. Find remote jobs, full-time positions, and more.')">
|
||||
<meta name="keywords" content="@yield('keywords', 'jobs, careers, employment, job search, remote jobs')">
|
||||
|
||||
<!-- Canonical URL -->
|
||||
<link rel="canonical" href="@yield('canonical', url()->current())">
|
||||
|
||||
<!-- Open Graph Meta Tags -->
|
||||
<meta property="og:title" content="@yield('og_title', '@yield('title', 'JobifyGarden - Find Your Dream Job')')">
|
||||
<meta property="og:description" content="@yield('og_description', '@yield('description', 'Discover thousands of job opportunities from multiple sources.')')">
|
||||
<meta property="og:url" content="@yield('og_url', url()->current())">
|
||||
<meta property="og:type" content="@yield('og_type', 'website')">
|
||||
<meta property="og:image" content="@yield('og_image', asset('images/og-default.jpg'))">
|
||||
<meta property="og:site_name" content="JobifyGarden">
|
||||
|
||||
<!-- Twitter Card Meta Tags -->
|
||||
<meta name="twitter:card" content="@yield('twitter_card', 'summary_large_image')">
|
||||
<meta name="twitter:title" content="@yield('twitter_title', '@yield('title', 'JobifyGarden')')">
|
||||
<meta name="twitter:description" content="@yield('twitter_description', '@yield('description', 'Find your dream job')')">
|
||||
<meta name="twitter:image" content="@yield('twitter_image', asset('images/twitter-card-default.jpg'))">
|
||||
|
||||
<!-- Structured Data (JSON-LD) -->
|
||||
@stack('structured_data')
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use App\Http\Controllers\Admin\DashboardController;
|
||||
use App\Http\Controllers\Web\HomeController;
|
||||
use App\Http\Controllers\Web\JobController;
|
||||
use App\Http\Controllers\Web\SitemapController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Homepage
|
||||
@@ -16,6 +17,12 @@ Route::prefix('jobs')->group(function () {
|
||||
Route::get('/{id}', [JobController::class, 'show'])->name('jobs.show');
|
||||
});
|
||||
|
||||
// Sitemap routes
|
||||
Route::get('/sitemap.xml', [SitemapController::class, 'index'])->name('sitemap');
|
||||
Route::get('/sitemap-index.xml', [SitemapController::class, 'sitemapIndex'])->name('sitemap.index');
|
||||
Route::get('/sitemap-main.xml', [SitemapController::class, 'mainSitemap'])->name('sitemap.main');
|
||||
Route::get('/sitemap-jobs.xml', [SitemapController::class, 'jobsSitemap'])->name('sitemap.jobs');
|
||||
|
||||
// Admin routes (in production, protect these with authentication middleware)
|
||||
Route::prefix('admin')->name('admin.')->group(function () {
|
||||
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||
|
||||
Reference in New Issue
Block a user