Automate SEO Audits in Laravel with the SEOPeek API
Laravel applications grow fast. New routes, Blade templates, dynamic content from Nova or Filament—every change is a chance for SEO regressions to slip through. Missing meta descriptions, duplicate title tags, broken Open Graph images—these problems compound silently until traffic drops. This guide shows you how to wire the SEOPeek API into your Laravel project with an Artisan command, middleware, scheduled tasks, database storage, and a Blade dashboard so every page is audited automatically and every regression is caught before it reaches production.
- Why automated SEO audits matter for Laravel apps
- The traditional approach and its downsides
- Calling the SEOPeek API with Laravel HTTP client
- Artisan command: php artisan seo:audit
- Middleware for dev/staging SEO checks
- Scheduled weekly audits
- Storing results + Blade dashboard
- Handling the JSON response
- Slack and email notifications
- FAQ
1. Why Automated SEO Audits Matter for Laravel Apps
Most Laravel teams check SEO once—when the site launches—and never again. But Laravel applications are dynamic. Blade components get refactored. CMS content changes daily. New routes appear and old ones redirect. Each change can silently break on-page SEO signals that Google relies on to rank your pages.
Automated audits catch these regressions the moment they happen, not weeks later when organic traffic has already dropped. By integrating SEO checks into your development workflow, you turn SEO from a reactive scramble into a continuous, automated process.
Here is what an automated Laravel SEO audit pipeline gives you:
- Instant regression detection — catch missing titles, broken canonical URLs, and empty meta descriptions before they reach production
- Historical tracking — store every audit score in your database and watch trends over time
- CI/CD integration — fail a deployment if any page scores below your SEO threshold
- Developer accountability — Slack notifications pinpoint exactly which page regressed and when
2. The Traditional Approach and Its Downsides
Before APIs like SEOPeek, teams relied on manual tools:
- Screaming Frog — powerful desktop crawler, but requires a GUI, a license ($259/year), and someone to remember to run it
- SEOptimer / Ahrefs Site Audit — SaaS dashboards with scheduled crawls, but they sit outside your deployment pipeline and cost $99+/month
- Manual checklists — a Google Doc with 30 checkboxes that nobody opens after sprint two
- Lighthouse — requires headless Chrome and Node.js, which is awkward to run in a PHP stack
None of these tools integrate natively with Laravel. They cannot run inside your Artisan commands, your middleware, or your test suite. The SEOPeek API solves this: it is a single HTTP endpoint that returns JSON. If your Laravel app can make an HTTP request, it can audit any page for SEO.
3. Calling the SEOPeek API with Laravel HTTP Client
Laravel ships with the Http facade, which wraps Guzzle internally. You do not need to install any additional packages. Create a service class to keep your API calls organized:
<?php
// app/Services/SeopeekService.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class SeopeekService
{
private string $baseUrl = 'https://us-central1-todd-agent-prod.cloudfunctions.net/seopeekApi/api/v1/audit';
public function audit(string $url): array
{
$response = Http::timeout(30)
->get($this->baseUrl, ['url' => $url]);
$response->throw(); // Throws on 4xx/5xx
return $response->json();
}
public function auditBulk(array $urls, int $delayMs = 500): array
{
$results = [];
foreach ($urls as $url) {
$results[$url] = $this->audit($url);
usleep($delayMs * 1000); // Be polite to the API
}
return $results;
}
}
Tip: Register SeopeekService as a singleton in your AppServiceProvider so you can inject it anywhere. Add your API key to .env when you upgrade to a paid plan.
If you prefer Guzzle directly (for older Laravel versions or non-Laravel PHP projects), install it with Composer:
composer require guzzlehttp/guzzle
But for Laravel 9+ the Http facade is the idiomatic choice and requires zero extra dependencies.
4. Artisan Command: php artisan seo:audit
This is the core of your Laravel automated SEO checks. The command accepts a list of URLs, audits each one, and prints a report. If any page falls below your minimum score, it exits with a non-zero code—perfect for CI/CD pipelines.
<?php
// app/Console/Commands/SeoAudit.php
namespace App\Console\Commands;
use App\Services\SeopeekService;
use Illuminate\Console\Command;
class SeoAudit extends Command
{
protected $signature = 'seo:audit
{urls?* : URLs to audit (space-separated)}
{--file= : Path to a file with one URL per line}
{--min-score=70 : Minimum acceptable SEO score}
{--json : Output results as JSON}';
protected $description = 'Run SEO audits on URLs using the SEOPeek API';
public function handle(SeopeekService $seopeek): int
{
$urls = $this->argument('urls');
// Load URLs from file if --file is provided
if ($file = $this->option('file')) {
if (!file_exists($file)) {
$this->error("File not found: {$file}");
return self::FAILURE;
}
$fileUrls = array_filter(
array_map('trim', file($file, FILE_IGNORE_NEW_LINES))
);
$urls = array_merge($urls, $fileUrls);
}
if (empty($urls)) {
$this->error('No URLs provided. Pass URLs as arguments or use --file.');
return self::FAILURE;
}
$minScore = (int) $this->option('min-score');
$results = [];
$failures = 0;
$this->info("Auditing " . count($urls) . " URL(s)...\n");
foreach ($urls as $url) {
try {
$result = $seopeek->audit($url);
$results[] = $result;
$score = $result['score'];
$grade = $result['grade'];
$status = $score >= $minScore ? '<fg=green>PASS</>' : '<fg=red>FAIL</>';
if ($score < $minScore) {
$failures++;
}
$this->line(" [{$status}] {$score}/100 ({$grade}) — {$url}");
// Show failing checks
foreach ($result['checks'] ?? [] as $name => $check) {
if (!$check['pass']) {
$this->line(" <fg=yellow>! {$name}: {$check['message']}</>");
}
}
} catch (\Exception $e) {
$this->error(" ERROR: {$url} — {$e->getMessage()}");
$failures++;
}
usleep(500_000); // 500ms delay between requests
}
if ($this->option('json')) {
$this->line(json_encode($results, JSON_PRETTY_PRINT));
}
$this->newLine();
$this->info("Done. {$failures} failure(s) out of " . count($urls) . " URL(s).");
return $failures > 0 ? self::FAILURE : self::SUCCESS;
}
}
Run it from your terminal:
# Audit specific URLs
php artisan seo:audit https://yourapp.com https://yourapp.com/pricing
# Audit URLs from a file
php artisan seo:audit --file=storage/seo-urls.txt --min-score=80
# Use in CI/CD (non-zero exit code on failure)
php artisan seo:audit --file=storage/seo-urls.txt --min-score=75 || exit 1
5. Laravel Middleware for Dev/Staging SEO Checks
This middleware runs a Laravel middleware SEO check on every HTML response in your local or staging environment. It appends an HTML comment with the SEO score to the bottom of each page, so you can see issues as you develop.
<?php
// app/Http/Middleware/SeoAuditMiddleware.php
namespace App\Http\Middleware;
use App\Services\SeopeekService;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class SeoAuditMiddleware
{
public function __construct(private SeopeekService $seopeek)
{
}
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// Only run in dev/staging, only on HTML responses
if (!App::environment('local', 'staging')) {
return $response;
}
if (!str_contains($response->headers->get('Content-Type', ''), 'text/html')) {
return $response;
}
try {
$url = $request->fullUrl();
$result = $this->seopeek->audit($url);
$score = $result['score'];
$grade = $result['grade'];
$failCount = collect($result['checks'] ?? [])
->filter(fn($c) => !$c['pass'])
->count();
$comment = "\n<!-- SEOPeek: {$score}/100 ({$grade}) | {$failCount} issue(s) -->";
$content = $response->getContent();
$response->setContent($content . $comment);
if ($score < 60) {
Log::warning("SEO score below 60: {$score} on {$url}");
}
} catch (\Exception $e) {
Log::debug("SEOPeek middleware error: {$e->getMessage()}");
}
return $response;
}
}
Register it in your app/Http/Kernel.php (Laravel 10) or bootstrap/app.php (Laravel 11):
// Laravel 10: app/Http/Kernel.php
protected $middlewareGroups = [
'web' => [
// ... other middleware
\App\Http\Middleware\SeoAuditMiddleware::class,
],
];
// Laravel 11: bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\App\Http\Middleware\SeoAuditMiddleware::class,
]);
})
Important: The middleware checks App::environment('local', 'staging') so it will never run in production. But for extra safety, you can also wrap the registration in an environment check.
6. Scheduled Weekly SEO Audits
Laravel's task scheduler makes it trivial to run your PHP SEO audit API checks on a recurring basis. Add this to your console routes:
<?php
// app/Console/Kernel.php (Laravel 10)
protected function schedule(Schedule $schedule): void
{
$schedule->command('seo:audit', [
'--file' => storage_path('seo-urls.txt'),
'--min-score' => 70,
])
->weekly()
->sundays()
->at('06:00')
->emailOutputOnFailure('team@yourcompany.com')
->description('Weekly SEO audit via SEOPeek');
}
// Laravel 11: routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('seo:audit', [
'--file' => storage_path('seo-urls.txt'),
'--min-score' => 70,
])
->weekly()
->sundays()
->at('06:00')
->emailOutputOnFailure('team@yourcompany.com')
->description('Weekly SEO audit via SEOPeek');
Make sure your server's crontab includes the Laravel scheduler entry:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
7. Storing Results and Building a Blade Dashboard
To track SEO trends over time, store every audit result in your database. Start with a migration:
php artisan make:migration create_seo_audits_table
<?php
// database/migrations/xxxx_create_seo_audits_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::create('seo_audits', function (Blueprint $table) {
$table->id();
$table->string('url', 2048);
$table->unsignedTinyInteger('score');
$table->string('grade', 4);
$table->json('checks')->nullable();
$table->json('failing_checks')->nullable();
$table->timestamps();
$table->index('url');
$table->index('created_at');
});
}
public function down(): void
{
Schema::dropIfExists('seo_audits');
}
};
Create the Eloquent model:
<?php
// app/Models/SeoAudit.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SeoAudit extends Model
{
protected $fillable = ['url', 'score', 'grade', 'checks', 'failing_checks'];
protected $casts = [
'checks' => 'array',
'failing_checks' => 'array',
];
public function scopeFailing($query, int $threshold = 70)
{
return $query->where('score', '<', $threshold);
}
public function scopeForUrl($query, string $url)
{
return $query->where('url', $url);
}
}
Update your SeopeekService to store results after each audit:
// In SeopeekService::audit(), after getting the response:
$data = $response->json();
\App\Models\SeoAudit::create([
'url' => $url,
'score' => $data['score'],
'grade' => $data['grade'],
'checks' => $data['checks'] ?? [],
'failing_checks' => collect($data['checks'] ?? [])
->filter(fn($c) => !$c['pass'])
->keys()
->toArray(),
]);
Now build a simple Blade view to display the results:
<!-- resources/views/seo/dashboard.blade.php -->
@extends('layouts.app')
@section('content')
<div class="container mx-auto py-8">
<h1 class="text-2xl font-bold mb-6">SEO Audit Dashboard</h1>
<table class="w-full text-sm">
<thead>
<tr class="border-b">
<th class="text-left py-2">URL</th>
<th class="text-center py-2">Score</th>
<th class="text-center py-2">Grade</th>
<th class="text-center py-2">Issues</th>
<th class="text-right py-2">Audited</th>
</tr>
</thead>
<tbody>
@foreach ($audits as $audit)
<tr class="border-b {{ $audit->score < 70 ? 'bg-red-50' : '' }}">
<td class="py-2 truncate max-w-xs">{{ $audit->url }}</td>
<td class="text-center py-2 font-mono">{{ $audit->score }}</td>
<td class="text-center py-2">{{ $audit->grade }}</td>
<td class="text-center py-2">{{ count($audit->failing_checks ?? []) }}</td>
<td class="text-right py-2 text-gray-500">{{ $audit->created_at->diffForHumans() }}</td>
</tr>
@endforeach
</tbody>
</table>
{{ $audits->links() }}
</div>
@endsection
And the controller:
<?php
// app/Http/Controllers/SeoController.php
namespace App\Http\Controllers;
use App\Models\SeoAudit;
class SeoController extends Controller
{
public function dashboard()
{
$audits = SeoAudit::latest()
->paginate(25);
return view('seo.dashboard', compact('audits'));
}
}
8. Handling the JSON Response
The SEOPeek API returns a structured JSON object. Here is how to work with every field:
$result = $seopeek->audit('https://yourapp.com/pricing');
// Top-level fields
$score = $result['score']; // Integer 0-100
$grade = $result['grade']; // A+, A, B+, B, C, D, F
// Individual checks (each has 'pass' and 'message')
$checks = $result['checks'];
// Get only failing checks
$failing = collect($checks)
->filter(fn($check) => !$check['pass']);
// Common checks the API returns:
// title, meta_description, h1, og_tags, twitter_card,
// canonical, robots_txt, structured_data, image_alt,
// viewport, lang_attribute, https, mobile_friendly
// Score interpretation:
// 90-100 = A/A+ (excellent)
// 80-89 = B/B+ (good, minor issues)
// 70-79 = C (needs attention)
// 60-69 = D (significant issues)
// 0-59 = F (critical problems)
9. Slack and Email Notifications for Failing Audits
Wire up Laravel notifications so your team is alerted instantly when SEO degrades. Create a notification class:
<?php
// app/Notifications/SeoAuditFailed.php
namespace App\Notifications;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class SeoAuditFailed extends Notification
{
public function __construct(
private array $results,
private int $threshold
) {}
public function via($notifiable): array
{
return ['slack', 'mail'];
}
public function toSlack($notifiable): SlackMessage
{
$failing = collect($this->results)
->filter(fn($r) => $r['score'] < $this->threshold);
$lines = $failing->map(
fn($r) => "{$r['score']}/100 ({$r['grade']}) — {$r['url']}"
)->join("\n");
return (new SlackMessage)
->error()
->content("SEO Audit: {$failing->count()} page(s) below {$this->threshold}")
->attachment(function ($attachment) use ($lines) {
$attachment->content($lines);
});
}
public function toMail($notifiable): MailMessage
{
$failing = collect($this->results)
->filter(fn($r) => $r['score'] < $this->threshold);
return (new MailMessage)
->subject("SEO Audit Alert: {$failing->count()} page(s) below threshold")
->line("{$failing->count()} page(s) scored below {$this->threshold}/100:")
->line($failing->map(
fn($r) => "- {$r['url']}: {$r['score']}/100 ({$r['grade']})"
)->join("\n"))
->action('View Dashboard', url('/seo/dashboard'));
}
}
Dispatch the notification at the end of your Artisan command when failures are detected:
// At the end of SeoAudit::handle()
if ($failures > 0) {
Notification::route('slack', config('services.slack.seo_webhook'))
->route('mail', 'team@yourcompany.com')
->notify(new SeoAuditFailed($results, $minScore));
}
Get 50 Free SEO Audits Per Day
The SEOPeek API is free for up to 50 audits/day. No API key required. Start integrating in minutes.
View Pricing & Get Started →FAQ
Can I use the SEOPeek API with Laravel without installing Guzzle?
Yes. Laravel ships with the Http facade (Illuminate\Support\Facades\Http), which wraps Guzzle internally. You do not need to install anything extra. Just use Http::get() to call the SEOPeek API endpoint.
How many free SEO audits does SEOPeek allow per day?
The free tier includes 50 audits per day with no API key required. For higher volumes, the Starter plan provides 1,000 audits/day at $9/month and the Pro plan provides 10,000 audits/day at $29/month.
Is the SEO middleware safe to run in production?
The middleware shown in this guide checks App::environment('local', 'staging') before running. It should never be enabled in production because it adds an HTTP round-trip to every response. Use it only in development or staging environments.
How does SEOPeek compare to running Lighthouse in PHP?
Lighthouse requires a headless Chrome instance and Node.js, which adds significant complexity to a PHP stack. SEOPeek is a single HTTP GET request that returns JSON in under 2 seconds. No browser dependencies, no Node.js, no Puppeteer setup required.
Can I run SEOPeek audits in my Laravel CI/CD pipeline?
Absolutely. Create the Artisan command shown in this guide and run it in your CI pipeline with php artisan seo:audit --file=storage/seo-urls.txt --min-score=70. If any page falls below the threshold, the command exits with a non-zero code and fails the build.
The Peek Suite
SEOPeek is part of a family of developer-focused audit tools.