Skip to content

Laravel Integration

Rumen Damyanov edited this page Sep 22, 2025 · 2 revisions

Laravel Framework Examples

This guide provides comprehensive examples for integrating the PHP-SEO package with Laravel applications, following Laravel best practices and conventions.

Quick Start

Installation

Install the package via Composer:

composer require rumenx/php-seo

Laravel Service Provider

Create a custom service provider for the PHP-SEO package:

php artisan make:provider SeoServiceProvider

Register the service provider in app/Providers/SeoServiceProvider.php:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Rumenx\PhpSeo\Config\SeoConfig;
use Rumenx\PhpSeo\SeoManager;

class SeoServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(SeoConfig::class, function ($app) {
            return new SeoConfig([
                'ai' => [
                    'enabled' => config('seo.ai.enabled', false),
                    'provider' => config('seo.ai.provider', 'openai'),
                ],
                'providers' => [
                    'openai' => [
                        'api_key' => config('seo.providers.openai.api_key'),
                        'model' => config('seo.providers.openai.model', 'gpt-3.5-turbo'),
                    ],
                ],
                'cache' => [
                    'enabled' => config('seo.cache.enabled', true),
                    'ttl' => config('seo.cache.ttl', 3600),
                ],
            ]);
        });

        $this->app->singleton(SeoManager::class, function ($app) {
            return new SeoManager($app->make(SeoConfig::class));
        });
    }

    public function boot(): void
    {
        $this->publishes([
            __DIR__.'/../../config/seo.php' => config_path('seo.php'),
        ], 'seo-config');
    }
}

Register the provider in config/app.php:

'providers' => [
    // Other providers...
    App\Providers\SeoServiceProvider::class,
],

Configuration File

Create a configuration file config/seo.php:

<?php

return [
    'ai' => [
        'enabled' => env('SEO_AI_ENABLED', false),
        'provider' => env('SEO_AI_PROVIDER', 'openai'),
        'fallback_enabled' => env('SEO_AI_FALLBACK_ENABLED', true),
    ],
    
    'providers' => [
        'openai' => [
            'api_key' => env('OPENAI_API_KEY'),
            'model' => env('OPENAI_MODEL', 'gpt-3.5-turbo'),
        ],
        'anthropic' => [
            'api_key' => env('ANTHROPIC_API_KEY'),
            'model' => env('ANTHROPIC_MODEL', 'claude-3-sonnet-20240229'),
        ],
    ],
    
    'cache' => [
        'enabled' => env('SEO_CACHE_ENABLED', true),
        'ttl' => env('SEO_CACHE_TTL', 3600),
        'driver' => env('SEO_CACHE_DRIVER', 'redis'),
    ],
    
    'sitemap' => [
        'enabled' => env('SEO_SITEMAP_ENABLED', true),
        'path' => env('SEO_SITEMAP_PATH', 'sitemap.xml'),
        'auto_generate' => env('SEO_SITEMAP_AUTO_GENERATE', true),
    ],
];

Add to your .env file:

SEO_AI_ENABLED=true
SEO_AI_PROVIDER=openai
OPENAI_API_KEY=your-openai-api-key
SEO_CACHE_ENABLED=true
SEO_SITEMAP_ENABLED=true

Model Integration

Eloquent Model Trait

Create a trait for automatic SEO generation:

<?php

namespace App\Traits;

use Rumenx\PhpSeo\SeoManager;

trait HasSeo
{
    protected static function bootHasSeo(): void
    {
        static::saving(function ($model) {
            if ($model->auto_generate_seo ?? true) {
                $seoManager = app(SeoManager::class);
                $model->generateSeoContent($seoManager);
            }
        });
    }

    public function generateSeoContent(SeoManager $seoManager): void
    {
        $content = $this->getSeoContent();
        $analysis = $seoManager->analyze($content);

        if (empty($this->meta_title)) {
            $this->meta_title = $seoManager->generateTitle($analysis);
        }

        if (empty($this->meta_description)) {
            $this->meta_description = $seoManager->generateDescription($analysis);
        }

        if (empty($this->meta_keywords)) {
            $keywords = $seoManager->generateKeywords($analysis);
            $this->meta_keywords = implode(', ', $keywords);
        }
    }

    abstract protected function getSeoContent(): string;

    public function getSeoTitle(): string
    {
        return $this->meta_title ?: $this->title;
    }

    public function getSeoDescription(): string
    {
        return $this->meta_description ?: str_limit(strip_tags($this->content), 160);
    }

    public function getSeoKeywords(): array
    {
        return $this->meta_keywords ? explode(', ', $this->meta_keywords) : [];
    }
}

Article Model Example

<?php

namespace App\Models;

use App\Traits\HasSeo;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Article extends Model
{
    use HasFactory, HasSeo;

    protected $fillable = [
        'title',
        'slug',
        'content',
        'excerpt',
        'published_at',
        'meta_title',
        'meta_description',
        'meta_keywords',
        'auto_generate_seo',
    ];

    protected $casts = [
        'published_at' => 'datetime',
        'auto_generate_seo' => 'boolean',
    ];

    protected function getSeoContent(): string
    {
        return $this->title . ' ' . $this->excerpt . ' ' . strip_tags($this->content);
    }

    public function getRouteKeyName(): string
    {
        return 'slug';
    }

    public function scopePublished($query)
    {
        return $query->whereNotNull('published_at')
                    ->where('published_at', '<=', now());
    }
}

Migration for SEO Fields

Create a migration to add SEO fields:

php artisan make:migration add_seo_fields_to_articles_table
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('articles', function (Blueprint $table) {
            $table->string('meta_title')->nullable();
            $table->text('meta_description')->nullable();
            $table->text('meta_keywords')->nullable();
            $table->boolean('auto_generate_seo')->default(true);
        });
    }

    public function down(): void
    {
        Schema::table('articles', function (Blueprint $table) {
            $table->dropColumn(['meta_title', 'meta_description', 'meta_keywords', 'auto_generate_seo']);
        });
    }
};

Controller Examples

Blog Controller

<?php

namespace App\Http\Controllers;

use App\Models\Article;
use Illuminate\Http\Request;
use Rumenx\PhpSeo\SeoManager;

class BlogController extends Controller
{
    public function __construct(
        private SeoManager $seoManager
    ) {}

    public function index(Request $request)
    {
        $articles = Article::published()
            ->orderBy('published_at', 'desc')
            ->paginate(10);

        // Generate SEO for blog index
        $seoData = $this->generateIndexSeo($request);

        return view('blog.index', compact('articles', 'seoData'));
    }

    public function show(Article $article)
    {
        // Generate or get SEO data for the article
        $seoData = $this->generateArticleSeo($article);

        return view('blog.show', compact('article', 'seoData'));
    }

    private function generateIndexSeo(Request $request): array
    {
        $page = $request->get('page', 1);
        $baseTitle = 'Blog - Latest Articles and Insights';
        
        if ($page > 1) {
            $title = "{$baseTitle} - Page {$page}";
        } else {
            $title = $baseTitle;
        }

        return [
            'title' => $title,
            'description' => 'Discover the latest articles, insights, and industry news on our blog.',
            'canonical' => url()->current(),
            'og_type' => 'website',
        ];
    }

    private function generateArticleSeo(Article $article): array
    {
        $content = $article->getSeoContent();
        $analysis = $this->seoManager->analyze($content);

        return [
            'title' => $article->getSeoTitle(),
            'description' => $article->getSeoDescription(),
            'keywords' => $article->getSeoKeywords(),
            'canonical' => route('blog.show', $article),
            'og_type' => 'article',
            'og_image' => $article->featured_image,
            'article_published_time' => $article->published_at?->toISOString(),
            'article_author' => $article->author?->name,
            'schema_org' => $this->generateArticleSchema($article, $analysis),
        ];
    }

    private function generateArticleSchema(Article $article, array $analysis): array
    {
        return [
            '@context' => 'https://schema.org',
            '@type' => 'Article',
            'headline' => $article->title,
            'description' => $article->getSeoDescription(),
            'image' => $article->featured_image,
            'datePublished' => $article->published_at?->toISOString(),
            'dateModified' => $article->updated_at?->toISOString(),
            'author' => [
                '@type' => 'Person',
                'name' => $article->author?->name,
            ],
            'publisher' => [
                '@type' => 'Organization',
                'name' => config('app.name'),
                'logo' => [
                    '@type' => 'ImageObject',
                    'url' => asset('images/logo.png'),
                ],
            ],
            'wordCount' => $analysis['word_count'] ?? 0,
            'keywords' => implode(', ', $article->getSeoKeywords()),
        ];
    }
}

API Controller

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Article;
use Illuminate\Http\Request;
use Rumenx\PhpSeo\SeoManager;

class SeoController extends Controller
{
    public function __construct(
        private SeoManager $seoManager
    ) {}

    public function analyze(Request $request)
    {
        $request->validate([
            'content' => 'required|string',
            'url' => 'nullable|url',
        ]);

        $analysis = $this->seoManager->analyze($request->content, [
            'url' => $request->url,
        ]);

        return response()->json([
            'success' => true,
            'data' => $analysis,
        ]);
    }

    public function generateTitle(Request $request)
    {
        $request->validate([
            'content' => 'required|string',
            'max_length' => 'nullable|integer|min:30|max:100',
            'use_ai' => 'nullable|boolean',
        ]);

        $analysis = $this->seoManager->analyze($request->content);
        $title = $this->seoManager->generateTitle($analysis, [
            'max_length' => $request->max_length ?? 60,
            'use_ai' => $request->use_ai ?? true,
        ]);

        return response()->json([
            'success' => true,
            'data' => [
                'title' => $title,
                'length' => strlen($title),
            ],
        ]);
    }

    public function generateDescription(Request $request)
    {
        $request->validate([
            'content' => 'required|string',
            'max_length' => 'nullable|integer|min:120|max:200',
            'use_ai' => 'nullable|boolean',
        ]);

        $analysis = $this->seoManager->analyze($request->content);
        $description = $this->seoManager->generateDescription($analysis, [
            'max_length' => $request->max_length ?? 160,
            'use_ai' => $request->use_ai ?? true,
        ]);

        return response()->json([
            'success' => true,
            'data' => [
                'description' => $description,
                'length' => strlen($description),
            ],
        ]);
    }

    public function generateBulk(Request $request)
    {
        $request->validate([
            'articles' => 'required|array|max:10',
            'articles.*.id' => 'required|integer|exists:articles,id',
            'articles.*.force_regenerate' => 'nullable|boolean',
        ]);

        $results = [];

        foreach ($request->articles as $item) {
            $article = Article::find($item['id']);
            $forceRegenerate = $item['force_regenerate'] ?? false;

            if ($forceRegenerate || empty($article->meta_title) || empty($article->meta_description)) {
                $content = $article->getSeoContent();
                $analysis = $this->seoManager->analyze($content);

                $results[] = [
                    'id' => $article->id,
                    'title' => $this->seoManager->generateTitle($analysis),
                    'description' => $this->seoManager->generateDescription($analysis),
                    'keywords' => $this->seoManager->generateKeywords($analysis),
                ];
            }
        }

        return response()->json([
            'success' => true,
            'data' => $results,
        ]);
    }
}

Blade Components

SEO Head Component

Create a Blade component for SEO head tags:

php artisan make:component SeoHead
<?php

namespace App\View\Components;

use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;

class SeoHead extends Component
{
    public function __construct(
        public array $seo = []
    ) {}

    public function render(): View|Closure|string
    {
        return view('components.seo-head');
    }

    public function getTitle(): string
    {
        $title = $this->seo['title'] ?? config('app.name');
        $siteName = config('app.name');
        
        return $title === $siteName ? $title : "{$title} - {$siteName}";
    }

    public function getDescription(): string
    {
        return $this->seo['description'] ?? 'Default site description';
    }

    public function getCanonical(): string
    {
        return $this->seo['canonical'] ?? request()->url();
    }

    public function getOgImage(): string
    {
        return $this->seo['og_image'] ?? asset('images/default-og-image.jpg');
    }
}

Create the component view resources/views/components/seo-head.blade.php:

<title>{{ $getTitle() }}</title>
<meta name="description" content="{{ $getDescription() }}">

@if(!empty($seo['keywords']))
<meta name="keywords" content="{{ is_array($seo['keywords']) ? implode(', ', $seo['keywords']) : $seo['keywords'] }}">
@endif

<link rel="canonical" href="{{ $getCanonical() }}">

<!-- Open Graph Tags -->
<meta property="og:title" content="{{ $seo['title'] ?? config('app.name') }}">
<meta property="og:description" content="{{ $getDescription() }}">
<meta property="og:type" content="{{ $seo['og_type'] ?? 'website' }}">
<meta property="og:url" content="{{ $getCanonical() }}">
<meta property="og:image" content="{{ $getOgImage() }}">
<meta property="og:site_name" content="{{ config('app.name') }}">

<!-- Twitter Card Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ $seo['title'] ?? config('app.name') }}">
<meta name="twitter:description" content="{{ $getDescription() }}">
<meta name="twitter:image" content="{{ $getOgImage() }}">

@if(!empty($seo['article_published_time']))
<meta property="article:published_time" content="{{ $seo['article_published_time'] }}">
@endif

@if(!empty($seo['article_author']))
<meta property="article:author" content="{{ $seo['article_author'] }}">
@endif

@if(!empty($seo['schema_org']))
<script type="application/ld+json">
{!! json_encode($seo['schema_org'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) !!}
</script>
@endif

Usage in Layouts

Update your main layout resources/views/layouts/app.blade.php:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <x-seo-head :seo="$seoData ?? []" />

    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
    @yield('content')
</body>
</html>

Commands

SEO Analysis Command

Create an Artisan command for bulk SEO analysis:

php artisan make:command AnalyzeSeoCommand
<?php

namespace App\Console\Commands;

use App\Models\Article;
use Illuminate\Console\Command;
use Rumenx\PhpSeo\SeoManager;

class AnalyzeSeoCommand extends Command
{
    protected $signature = 'seo:analyze 
                          {--model=Article : The model to analyze}
                          {--force : Force regenerate existing SEO data}
                          {--chunk=50 : Number of records to process at once}';

    protected $description = 'Analyze and generate SEO data for models';

    public function handle(SeoManager $seoManager): int
    {
        $modelClass = "App\\Models\\{$this->option('model')}";
        
        if (!class_exists($modelClass)) {
            $this->error("Model {$modelClass} does not exist");
            return self::FAILURE;
        }

        $force = $this->option('force');
        $chunkSize = (int) $this->option('chunk');

        $query = $modelClass::query();
        
        if (!$force) {
            $query->where(function ($q) {
                $q->whereNull('meta_title')
                  ->orWhereNull('meta_description')
                  ->orWhere('meta_title', '')
                  ->orWhere('meta_description', '');
            });
        }

        $total = $query->count();
        
        if ($total === 0) {
            $this->info('No records need SEO generation');
            return self::SUCCESS;
        }

        $this->info("Processing {$total} records...");
        $bar = $this->output->createProgressBar($total);

        $query->chunk($chunkSize, function ($models) use ($seoManager, $bar) {
            foreach ($models as $model) {
                try {
                    $model->generateSeoContent($seoManager);
                    $model->save();
                    $bar->advance();
                } catch (\Exception $e) {
                    $this->error("Error processing {$model->id}: {$e->getMessage()}");
                }
            }
        });

        $bar->finish();
        $this->newLine();
        $this->info('SEO analysis completed!');

        return self::SUCCESS;
    }
}

Sitemap Generation Command

php artisan make:command GenerateSitemapCommand
<?php

namespace App\Console\Commands;

use App\Models\Article;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Rumenx\PhpSeo\SeoManager;

class GenerateSitemapCommand extends Command
{
    protected $signature = 'seo:sitemap {--output=public/sitemap.xml}';
    protected $description = 'Generate XML sitemap';

    public function handle(SeoManager $seoManager): int
    {
        $sitemap = $seoManager->generateSitemap([
            'domain' => config('app.url'),
            'routes' => $this->getRoutes(),
        ]);

        $outputPath = $this->option('output');
        file_put_contents($outputPath, $sitemap);

        $this->info("Sitemap generated: {$outputPath}");
        return self::SUCCESS;
    }

    private function getRoutes(): array
    {
        $routes = [];

        // Static pages
        $routes[] = [
            'url' => route('home'),
            'lastmod' => now()->toISOString(),
            'changefreq' => 'daily',
            'priority' => '1.0',
        ];

        // Blog articles
        Article::published()->chunk(100, function ($articles) use (&$routes) {
            foreach ($articles as $article) {
                $routes[] = [
                    'url' => route('blog.show', $article),
                    'lastmod' => $article->updated_at->toISOString(),
                    'changefreq' => 'weekly',
                    'priority' => '0.8',
                ];
            }
        });

        return $routes;
    }
}

Middleware

SEO Middleware

Create middleware for automatic SEO handling:

php artisan make:middleware SeoMiddleware
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Rumenx\PhpSeo\SeoManager;
use Symfony\Component\HttpFoundation\Response;

class SeoMiddleware
{
    public function __construct(
        private SeoManager $seoManager
    ) {}

    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        // Only process HTML responses
        if (!$response instanceof \Illuminate\Http\Response || 
            !str_contains($response->headers->get('Content-Type', ''), 'text/html')) {
            return $response;
        }

        $content = $response->getContent();
        
        // Analyze current page content
        $analysis = $this->seoManager->analyze($content);
        
        // Add SEO analysis to view data if needed
        if (view()->exists('partials.seo-debug') && config('app.debug')) {
            $debugContent = view('partials.seo-debug', compact('analysis'))->render();
            $content = str_replace('</body>', $debugContent . '</body>', $content);
            $response->setContent($content);
        }

        return $response;
    }
}

Register in app/Http/Kernel.php:

protected $middlewareGroups = [
    'web' => [
        // Other middleware...
        \App\Http\Middleware\SeoMiddleware::class,
    ],
];

Testing

Feature Tests

Create tests for SEO functionality:

php artisan make:test SeoTest
<?php

namespace Tests\Feature;

use App\Models\Article;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Rumenx\PhpSeo\SeoManager;
use Tests\TestCase;

class SeoTest extends TestCase
{
    use RefreshDatabase;

    private SeoManager $seoManager;

    protected function setUp(): void
    {
        parent::setUp();
        $this->seoManager = app(SeoManager::class);
    }

    public function test_article_generates_seo_on_save(): void
    {
        $article = Article::create([
            'title' => 'Test Article',
            'content' => '<p>This is test content for SEO generation.</p>',
            'slug' => 'test-article',
            'auto_generate_seo' => true,
        ]);

        $this->assertNotNull($article->meta_title);
        $this->assertNotNull($article->meta_description);
        $this->assertNotEmpty($article->meta_keywords);
    }

    public function test_blog_show_page_has_proper_seo(): void
    {
        $article = Article::factory()->create([
            'title' => 'Laravel SEO Best Practices',
            'meta_title' => 'Laravel SEO Guide - Best Practices',
            'meta_description' => 'Learn Laravel SEO best practices for better search rankings.',
        ]);

        $response = $this->get(route('blog.show', $article));

        $response->assertStatus(200);
        $response->assertSee('Laravel SEO Guide - Best Practices', false);
        $response->assertSee('Learn Laravel SEO best practices', false);
    }

    public function test_seo_api_endpoints(): void
    {
        $content = '<h1>Test Title</h1><p>Test content for analysis.</p>';

        // Test analyze endpoint
        $response = $this->postJson('/api/seo/analyze', [
            'content' => $content,
        ]);

        $response->assertStatus(200)
                ->assertJsonStructure([
                    'success',
                    'data' => [
                        'word_count',
                        'title_tags',
                        'meta_description',
                        'headings',
                    ],
                ]);

        // Test title generation
        $response = $this->postJson('/api/seo/generate-title', [
            'content' => $content,
            'max_length' => 60,
        ]);

        $response->assertStatus(200)
                ->assertJsonStructure([
                    'success',
                    'data' => [
                        'title',
                        'length',
                    ],
                ]);
    }

    public function test_sitemap_generation(): void
    {
        Article::factory()->count(5)->create();

        $this->artisan('seo:sitemap')
             ->expectsOutput('Sitemap generated: public/sitemap.xml')
             ->assertExitCode(0);

        $this->assertFileExists(public_path('sitemap.xml'));
        
        $sitemap = file_get_contents(public_path('sitemap.xml'));
        $this->assertStringContains('<?xml version="1.0" encoding="UTF-8"?>', $sitemap);
        $this->assertStringContains('<urlset', $sitemap);
    }
}

Unit Tests

php artisan make:test SeoManagerTest --unit
<?php

namespace Tests\Unit;

use Rumenx\PhpSeo\Config\SeoConfig;
use Rumenx\PhpSeo\SeoManager;
use Tests\TestCase;

class SeoManagerTest extends TestCase
{
    private SeoManager $seoManager;

    protected function setUp(): void
    {
        parent::setUp();
        
        $config = new SeoConfig([
            'ai' => ['enabled' => false], // Disable AI for unit tests
        ]);
        
        $this->seoManager = new SeoManager($config);
    }

    public function test_content_analysis(): void
    {
        $content = '<h1>Test Article</h1><p>This is a test article with some content.</p>';
        $analysis = $this->seoManager->analyze($content);

        $this->assertIsArray($analysis);
        $this->assertArrayHasKey('word_count', $analysis);
        $this->assertArrayHasKey('title_tags', $analysis);
        $this->assertEquals(1, count($analysis['title_tags']));
        $this->assertEquals('Test Article', $analysis['title_tags'][0]['text']);
    }

    public function test_title_generation_without_ai(): void
    {
        $content = '<h1>Laravel Best Practices</h1><p>Learn Laravel development best practices.</p>';
        $analysis = $this->seoManager->analyze($content);
        
        $title = $this->seoManager->generateTitle($analysis, ['use_ai' => false]);
        
        $this->assertIsString($title);
        $this->assertLessThanOrEqual(60, strlen($title));
    }

    public function test_description_generation_without_ai(): void
    {
        $content = '<h1>Laravel Guide</h1><p>Complete guide to Laravel development.</p>';
        $analysis = $this->seoManager->analyze($content);
        
        $description = $this->seoManager->generateDescription($analysis, ['use_ai' => false]);
        
        $this->assertIsString($description);
        $this->assertLessThanOrEqual(160, strlen($description));
    }
}

Deployment

Environment Configuration

Production .env settings:

# SEO Configuration
SEO_AI_ENABLED=true
SEO_AI_PROVIDER=openai
SEO_CACHE_ENABLED=true
SEO_CACHE_DRIVER=redis
SEO_SITEMAP_ENABLED=true

# API Keys (use secure storage)
OPENAI_API_KEY=your-production-openai-key
ANTHROPIC_API_KEY=your-production-anthropic-key

# Cache Settings
CACHE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

Scheduler Setup

Add to app/Console/Kernel.php:

protected function schedule(Schedule $schedule): void
{
    // Generate sitemap daily
    $schedule->command('seo:sitemap')
             ->daily()
             ->at('02:00');

    // Analyze new content hourly
    $schedule->command('seo:analyze --model=Article')
             ->hourly();
}

Queue Configuration

For high-volume sites, consider queueing SEO generation:

php artisan make:job GenerateSeoJob
<?php

namespace App\Jobs;

use App\Models\Article;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Rumenx\PhpSeo\SeoManager;

class GenerateSeoJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        private int $articleId
    ) {}

    public function handle(SeoManager $seoManager): void
    {
        $article = Article::find($this->articleId);
        
        if ($article) {
            $article->generateSeoContent($seoManager);
            $article->save();
        }
    }
}

Dispatch the job in your model:

protected static function bootHasSeo(): void
{
    static::saved(function ($model) {
        if ($model->auto_generate_seo ?? true) {
            GenerateSeoJob::dispatch($model->id);
        }
    });
}

Performance Optimization

Caching Strategy

Implement caching for SEO data:

// In your controller
public function show(Article $article)
{
    $cacheKey = "article_seo_{$article->id}_{$article->updated_at->timestamp}";
    
    $seoData = Cache::remember($cacheKey, 3600, function () use ($article) {
        return $this->generateArticleSeo($article);
    });

    return view('blog.show', compact('article', 'seoData'));
}

Database Optimization

Add indexes for SEO queries:

Schema::table('articles', function (Blueprint $table) {
    $table->index(['published_at', 'meta_title']);
    $table->index('slug');
});

Next Steps

Clone this wiki locally