Installation
composer require tipoff/seo
Publish the config file:
php artisan vendor:publish --provider="Tipoff\Seo\SeoServiceProvider" --tag="config"
Configuration
Edit config/seo.php to define:
keywords (array of tracked keywords)models (ELOQUENT models to track keyword associations)middleware (optional: Tipoff\Seo\Middleware\TrackKeywords)First Use Case
Track a keyword for a model (e.g., Post):
use Tipoff\Seo\Traits\HasKeywords;
class Post extends Model
{
use HasKeywords;
}
// In a controller or model:
$post = Post::find(1);
$post->trackKeyword('laravel seo'); // Tracks the keyword for this post
Tracking Keywords
trackKeyword() on any model with the HasKeywords trait.
$model->trackKeyword('keyword phrase');
// In config/seo.php:
'middleware' => ['web'],
Querying Models by Keywords
$posts = Post::whereKeyword('laravel seo')->get();
if ($post->hasKeyword('laravel seo')) {
// ...
}
Bulk Operations
$posts = Post::where('published', true)->get();
$posts->each->trackKeyword('popular laravel topics');
Analytics Integration
Seo facade to fetch keyword analytics:
use Tipoff\Seo\Facades\Seo;
$stats = Seo::getKeywordStats('laravel seo');
// Returns: ['count' => 42, 'models' => [...]]
Model Observers Track keywords automatically when models are created/updated:
class PostObserver
{
public function saved(Post $post)
{
if ($post->isPublished()) {
$post->trackKeyword('published content');
}
}
}
API Endpoints Expose keyword tracking via API:
Route::post('/posts/{post}/keywords', function (Post $post) {
$post->trackKeyword(request('keyword'));
return response()->json(['success' => true]);
});
SEO Reports Generate reports in a controller:
public function seoReport()
{
$keywords = Seo::getAllKeywords();
$report = collect($keywords)->mapWithKeys(function ($keyword) {
return [$keyword => Seo::getKeywordStats($keyword)];
});
return view('reports.seo', compact('report'));
}
Caching Cache keyword stats to improve performance:
$stats = Cache::remember("seo:stats:{$keyword}", now()->addHours(1), function () use ($keyword) {
return Seo::getKeywordStats($keyword);
});
Case Sensitivity Keywords are stored in lowercase by default. Ensure consistency:
// Avoid:
$post->trackKeyword('Laravel SEO'); // Stored as 'laravel seo'
$post->hasKeyword('laravel seo'); // Works
$post->hasKeyword('Laravel SEO'); // Fails (case mismatch)
Fix: Normalize keywords before tracking:
$post->trackKeyword(strtolower(request('keyword')));
Model Configuration
Forgetting to add models to config/seo.php under the models key will cause HasKeywords to fail silently.
Fix: Verify the config:
'models' => [
'App\Models\Post',
'App\Models\Page',
],
Middleware Conflicts
The TrackKeywords middleware may interfere with other middleware (e.g., auth) if not ordered correctly.
Fix: Register middleware in the correct group (e.g., web):
// app/Http/Kernel.php
protected $middlewareGroups = [
'web' => [
// ...
\Tipoff\Seo\Middleware\TrackKeywords::class,
],
];
Duplicate Keywords Tracking the same keyword multiple times on a model doesn’t create duplicates but may skew analytics. Fix: Check for existing associations:
if (!$post->hasKeyword('laravel seo')) {
$post->trackKeyword('laravel seo');
}
Log Keyword Tracking
Enable logging in config/seo.php:
'log_tracking' => env('SEO_LOG_TRACKING', false),
Check storage/logs/laravel.log for tracking events.
Database Inspection
Keywords are stored in the seo_keywords table. Verify entries with:
php artisan tinker
>>> \Tipoff\Seo\Models\Keyword::all();
Middleware Debugging Temporarily disable middleware to isolate issues:
// In Kernel.php:
protected $middleware = [
// \Tipoff\Seo\Middleware\TrackKeywords::class, // Comment out
];
Custom Keyword Sources
Extend the Tipoff\Seo\Contracts\KeywordSource interface to pull keywords from external sources (e.g., Google Analytics):
class AnalyticsKeywordSource implements KeywordSource
{
public function getKeywords()
{
return $this->fetchFromGoogleAnalytics();
}
}
Register in config/seo.php:
'sources' => [
\App\Services\AnalyticsKeywordSource::class,
],
Custom Storage Override the default pivot table by binding a custom model:
class Post extends Model
{
use HasKeywords;
protected $keywordModel = \App\Models\CustomKeyword::class;
}
Events Listen for keyword tracking events:
event(new \Tipoff\Seo\Events\KeywordTracked($model, $keyword));
Register listeners in EventServiceProvider:
protected $listen = [
\Tipoff\Seo\Events\KeywordTracked::class => [
\App\Listeners\LogKeywordTrack::class,
],
];
API Resources Extend the default JSON response for keywords:
namespace App\Http\Resources;
use Tipoff\Seo\Models\Keyword;
use Illuminate\Http\Resources\Json\JsonResource;
class KeywordResource extends JsonResource
{
public function toArray($request)
{
return [
'name' => $this->name,
'count' => $this->models->count(),
'models' => $this->models->pluck('id'),
];
}
}
Override the facade method:
Seo::setResourceClass(KeywordResource::class);
How can I help you explore Laravel packages today?