From 96e8fa3859d4c8bdaf64be4887bb4d92bf0468b5 Mon Sep 17 00:00:00 2001 From: Doar Mihai Date: Fri, 1 May 2026 03:14:52 +0300 Subject: [PATCH] blog --- app/Http/Controllers/Web/BlogController.php | 25 +++++++ .../Controllers/Web/SitemapController.php | 17 ++++- app/Models/Article.php | 71 +++++++++++++++++++ app/Services/SitemapService.php | 29 ++++++++ ...026_05_01_000001_create_articles_table.php | 28 ++++++++ resources/views/blog/index.blade.php | 51 +++++++++++++ resources/views/blog/show.blade.php | 48 +++++++++++++ resources/views/layouts/app.blade.php | 8 +++ routes/web.php | 8 +++ 9 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Web/BlogController.php create mode 100644 app/Models/Article.php create mode 100644 database/migrations/2026_05_01_000001_create_articles_table.php create mode 100644 resources/views/blog/index.blade.php create mode 100644 resources/views/blog/show.blade.php diff --git a/app/Http/Controllers/Web/BlogController.php b/app/Http/Controllers/Web/BlogController.php new file mode 100644 index 0000000..916c277 --- /dev/null +++ b/app/Http/Controllers/Web/BlogController.php @@ -0,0 +1,25 @@ +orderByDesc('published_at') + ->paginate(12); + + return view('blog.index', compact('articles')); + } + + public function show(Article $article) + { + abort_unless($article->is_published, 404); + + return view('blog.show', compact('article')); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Web/SitemapController.php b/app/Http/Controllers/Web/SitemapController.php index 55d0942..58760ce 100644 --- a/app/Http/Controllers/Web/SitemapController.php +++ b/app/Http/Controllers/Web/SitemapController.php @@ -66,11 +66,26 @@ class SitemapController extends Controller { $xml = $this->cacheService->remember( 'sitemap:jobs', - 1800, // 30 minutes cache (jobs change more frequently) + 1800, fn() => $this->sitemapService->generateJobsSitemap() ); return response($xml, 200) ->header('Content-Type', 'application/xml'); } + + /** + * Generate and serve blog sitemap + */ + public function blogSitemap(): Response + { + $xml = $this->cacheService->remember( + 'sitemap:blog', + 3600, + fn() => $this->sitemapService->generateBlogSitemap() + ); + + return response($xml, 200) + ->header('Content-Type', 'application/xml'); + } } diff --git a/app/Models/Article.php b/app/Models/Article.php new file mode 100644 index 0000000..fa04cba --- /dev/null +++ b/app/Models/Article.php @@ -0,0 +1,71 @@ + 'datetime', + ]; + } + + protected static function boot(): void + { + parent::boot(); + + static::creating(function (Article $article) { + if (empty($article->slug)) { + $article->slug = static::generateUniqueSlug($article->title); + } + }); + + static::updating(function (Article $article) { + if ($article->isDirty('title') && empty($article->slug)) { + $article->slug = static::generateUniqueSlug($article->title); + } + }); + } + + public static function generateUniqueSlug(string $title): string + { + $baseSlug = Str::limit(Str::slug($title), 200, ''); + $slug = $baseSlug; + $counter = 1; + + while (static::where('slug', $slug)->exists()) { + $slug = $baseSlug . '-' . $counter; + $counter++; + } + + return $slug; + } + + public function scopePublished(Builder $query): Builder + { + return $query->whereNotNull('published_at')->where('published_at', '<=', now()); + } + + public function getIsPublishedAttribute(): bool + { + return $this->published_at !== null && $this->published_at->isPast(); + } + + public function getRouteKeyName(): string + { + return 'slug'; + } +} \ No newline at end of file diff --git a/app/Services/SitemapService.php b/app/Services/SitemapService.php index 3a0a2f5..7583371 100644 --- a/app/Services/SitemapService.php +++ b/app/Services/SitemapService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Models\Article; use App\Models\Job; use App\Models\JobCategory; use Illuminate\Support\Facades\URL; @@ -92,6 +93,9 @@ class SitemapService // Add jobs sitemap $this->addSitemapToIndex($xml, route('sitemap.jobs'), now()); + // Add blog sitemap + $this->addSitemapToIndex($xml, route('sitemap.blog'), now()); + return $xml->asXML(); } @@ -105,6 +109,31 @@ class SitemapService $sitemapElement->addChild('lastmod', $lastmod->format('Y-m-d')); } + /** + * Generate blog sitemap + */ + public function generateBlogSitemap(): string + { + $xml = new \SimpleXMLElement(''); + + $this->addUrl($xml, route('blog.index'), now(), 'daily', '0.8'); + + Article::published() + ->orderByDesc('published_at') + ->get(['slug', 'published_at', 'updated_at']) + ->each(function (Article $article) use ($xml) { + $this->addUrl( + $xml, + route('blog.show', $article), + $article->updated_at, + 'monthly', + '0.7' + ); + }); + + return $xml->asXML(); + } + /** * Generate main sitemap (static pages and categories) */ diff --git a/database/migrations/2026_05_01_000001_create_articles_table.php b/database/migrations/2026_05_01_000001_create_articles_table.php new file mode 100644 index 0000000..43d17a4 --- /dev/null +++ b/database/migrations/2026_05_01_000001_create_articles_table.php @@ -0,0 +1,28 @@ +id(); + $table->string('title'); + $table->string('slug')->unique(); + $table->text('excerpt')->nullable(); + $table->longText('body'); + $table->timestamp('published_at')->nullable(); + $table->timestamps(); + + $table->index('published_at'); + }); + } + + public function down(): void + { + Schema::dropIfExists('articles'); + } +}; \ No newline at end of file diff --git a/resources/views/blog/index.blade.php b/resources/views/blog/index.blade.php new file mode 100644 index 0000000..d322b89 --- /dev/null +++ b/resources/views/blog/index.blade.php @@ -0,0 +1,51 @@ +@extends('layouts.app') + +@section('title', 'Blog - JobifyGarden') +@section('description', 'Career advice, job search tips, and hiring insights from the JobifyGarden team.') +@section('canonical', route('blog.index')) + +@section('content') +
+
+

Blog

+

Career advice, job search tips, and hiring insights.

+
+ + @if($articles->isEmpty()) +
+

No articles published yet. Check back soon.

+
+ @else +
+ @foreach($articles as $article) + + @endforeach +
+ +
+ {{ $articles->links() }} +
+ @endif +
+@endsection \ No newline at end of file diff --git a/resources/views/blog/show.blade.php b/resources/views/blog/show.blade.php new file mode 100644 index 0000000..12d0eff --- /dev/null +++ b/resources/views/blog/show.blade.php @@ -0,0 +1,48 @@ +@extends('layouts.app') + +@section('title', $article->title . ' - JobifyGarden Blog') +@section('description', $article->excerpt ?? Str::limit(strip_tags($article->body), 155)) +@section('canonical', route('blog.show', $article)) + +@push('structured_data') + +@endpush + +@section('content') +
+ + + +
+
+
{{ $article->published_at->format('F j, Y') }}
+

{{ $article->title }}

+ @if($article->excerpt) +

{{ $article->excerpt }}

+ @endif +
+ +
+ {!! nl2br(e($article->body)) !!} +
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index dd2c9a6..fce961f 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -80,6 +80,10 @@ class="inline-flex items-center px-1 pt-1 text-sm font-medium {{ request()->routeIs('jobs.remote') ? 'border-b-2 border-emerald-500 text-gray-900' : 'text-gray-500 hover:text-gray-700 hover:border-gray-300' }}"> Remote Jobs + + Blog + @@ -139,6 +143,10 @@ class="block pl-3 pr-4 py-2 border-l-4 text-base font-medium {{ request()->routeIs('jobs.remote') ? 'border-emerald-500 text-emerald-700 bg-emerald-50' : 'border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700' }}"> Remote Jobs + + Blog +
@auth diff --git a/routes/web.php b/routes/web.php index 85c5e39..0a19784 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,6 +2,7 @@ use App\Http\Controllers\Admin\DashboardController; use App\Http\Controllers\ProfileController; +use App\Http\Controllers\Web\BlogController; use App\Http\Controllers\Web\BookmarkController; use App\Http\Controllers\Web\HomeController; use App\Http\Controllers\Web\JobController; @@ -26,11 +27,18 @@ Route::prefix('jobs')->group(function () { Route::get('/{job:slug}', [JobController::class, 'show'])->name('jobs.show'); }); +// Blog routes +Route::prefix('blog')->name('blog.')->group(function () { + Route::get('/', [BlogController::class, 'index'])->name('index'); + Route::get('/{article:slug}', [BlogController::class, 'show'])->name('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'); +Route::get('/sitemap-blog.xml', [SitemapController::class, 'blogSitemap'])->name('sitemap.blog'); // Authenticated user routes Route::middleware('auth')->group(function () {