rumenx/php-seo
A lightweight PHP SEO library for generating meta tags, Open Graph and Twitter Card data, canonical URLs, sitemaps, and robots directives. Simple fluent API, framework-agnostic, and easy to integrate into Laravel or any PHP app.
Install via Composer and confirm your PHP version matches the package requirement (8.1+ is typical for AI-SDK-backed libraries):
composer require rumenx/php-seo
Where to look first
README.md — the primary usage example for the entry class (often Rumenx\PhpSeo\Seo or an SeoManager/SeoOptimizer equivalent). Copy the constructor signature exactly; the package is framework-agnostic, so there is no facade autoloading magic to fall back on.src/Providers/ (or src/Ai/) — this is where the OpenAI/Anthropic/other AI drivers live. Check which providers and model names are actually supported before you wire config.config/ if present — provider selection, API key resolution, model, temperature, and max-token settings usually live here. If there's no published config, config is passed as a constructor array.First use case: generate meta for a blog post
The lowest-friction entry point is one-shot generation from raw content:
use Rumenx\PhpSeo\Seo;
$seo = new Seo([
'provider' => 'openai',
'api_key' => $_ENV['OPENAI_API_KEY'],
'model' => 'gpt-4o-mini',
]);
$result = $seo->generate($post->body); // or ->optimize(), ->analyze()
echo $result->title; // <= 60 chars ideally
echo $result->description; // <= 160 chars ideally
print_r($result->metaTags); // assoc array of name => content
Start with generate() on a single known article, dump the result, and inspect it before integrating. Verify the exact method names against the README — this package is young and pre-1.0 APIs move.
First integration target: a Laravel/Symfony controller or console command that takes existing content and returns a rendered <head> block. Do not start with batch processing; start with one page, eyeball the output, then scale.
Wrap it in a service, never call it from views. Because it's framework-agnostic, treat it like any third-party SDK:
final class SeoService
{
public function __construct(private Seo $seo) {}
public function forPost(Post $post): SeoResult
{
return Cache::rememberForever(
"seo:post:{$post->id}:".md5($post->body),
fn () => $this->seo->generate($post->body)
);
}
}
Bind Seo as a singleton in a service provider so the HTTP client and config are reused.
Cache by content hash, not by model ID. AI output is expensive and non-deterministic; the hash key naturally invalidates when the article is edited.
Queue everything that isn't a page render. Generation typically takes 1–5 seconds per call. Dispatch a job on model save (PostSaved → GenerateSeoMetadata) that writes results back into seo_title, seo_description, og_image_alt columns. This keeps request latency flat and gives you retries for free.
Hybrid fallback pipeline. Use deterministic heuristics (first 160 chars, H1 as title) as the default, and only overwrite with AI output when it passes validation (length, non-empty, no markdown fences). This is the single biggest reliability win:
$ai = $this->seoService->forPost($post);
$title = $this->validTitle($ai->title) ? $ai->title : Str::limit($post->title, 60);
Render through a Blade partial, not string concatenation. Emit meta tags via @foreach over the returned array and run values through e(). It keeps escaping and deduplication in one place:
@foreach ($meta as $name => $content)
<meta name="{{ $name }}" content="{{ $content }}">
@endforeach
Alt-text generation belongs in the upload/media pipeline. Trigger it after the image is stored, pass the image path/URL, and write the result to the media record. Pair it with a vision-capable model — text-only models will hallucinate descriptions.
Batch/backfill via a console command. Loop over records, process in chunks of 10–20, throttle between calls, and log failures per-record. Never foreach 5,000 posts in a single synchronous command.
Provider abstraction for local dev. Configure a cheap/fast model in local and testing, and the production model only in production. If the package allows a null/no-op provider, use it in tests so CI never spends money.
Framework integration tips
seo_metadata JSON column. Publish config if available with php artisan vendor:publish.services.yaml, inject into a SeoGenerator service, and use Messenger for async.It's pre-1.0 and low-adoption. Pin an exact version ("rumenx/php-seo": "0.x.y") and read the CHANGELOG before every bump. With near-zero dependents, you are effectively the QA team — expect to read src/ when the README is thin.
Non-deterministic output breaks tests. Never assert exact generated strings. Mock the underlying HTTP/AI client, or assert only on structural constraints (strlen <= 160, is_string, key presence).
Length limits are guidance, not guarantees. The model will sometimes return a 200-character description. Truncate defensively with multibyte-safe functions (mb_substr, Str::limit), and trim on a word boundary to avoid chopped words.
AI responses can be wrapped in markdown fences or JSON prologues. If the package asks the model for structured output, still validate. A json_decode returning null should fall through to your heuristic path, not throw a 500 on a page render.
Escape everything at render time. AI can and will echo HTML from the source content. If you inject content into a meta tag without htmlspecialchars/e(), you have an XSS vector.
Cost and rate limits. Add a per-run token/request budget, honor 429 responses with exponential backoff, and log token usage per generation so you can attribute cost to features. One un-cached page render per request is the classic way to burn a monthly budget in an afternoon.
API key resolution. Prefer environment variables over constructor args committed to config files. Confirm the package's precedence order (env vs. constructor vs. config file) — mismatches here cause "works locally, fails in prod."
Watch for silent provider failures. AI SDKs often return an empty string rather than throwing. Always check for empty/null output before persisting; otherwise you'll overwrite good metadata with blanks during a provider outage.
Debugging checklist when generation returns garbage: (1) dump the raw response before parsing, (2) confirm you're passing the article body and not the HTML wrapper/nav, (3) check the model name is still valid — providers deprecate models regularly, (4) verify temperature isn't set high, (5) confirm your prompt/template isn't being overridden by config.
Extension points to look for: a provider interface you can implement for a custom LLM endpoint, configurable prompt templates (so you can enforce brand voice and output format), and any events/hooks fired around generation. Custom prompts are usually the highest-leverage customization — pinning exact output format ("respond with JSON: title, description, keywords") removes most of the parsing fragility.
Don't let AI own canonical/robots tags. Generate titles, descriptions, keywords, OG tags, and alt text with AI; keep canonical, robots, hreflang, and structured data (application/ld+json) deterministic. Those are SEO correctness issues, not creative ones.
How can I help you explore Laravel packages today?