The wrong question, and the right one
"What is the Yoast of Laravel?" is the most common question I get from WordPress developers moving to a framework. It has no good answer, because Yoast solves a problem Laravel does not have: WordPress ships a themeable output layer that no single developer controls, so metadata has to be bolted on through hooks and a UI. In Laravel you own the response. The right question is which SEO concerns need a package, which need a small service class, and which need a database column.
What an SEO layer actually has to do
- Emit a unique
<title>and meta description per route. - Emit a self-referencing canonical, correct under pagination and filtering.
- Emit Open Graph and Twitter tags with a real image.
- Emit valid JSON-LD matching what the page shows.
- Produce an accurate XML sitemap and robots.txt.
- Let editors override any of the above per record.
That is six concerns. None of them requires a plugin ecosystem.
Pattern 1: a view-composed meta service
Create a single Seo service bound in the container, with fluent setters for title, description, canonical, robots and image. Call it in the controller, render it from one Blade partial included in your layout head. Every page now has exactly one place where its metadata is decided, and a missing title becomes a test failure rather than a silent duplicate.
Add defaults at the service level: site name suffix, fallback description, fallback social image. Add a guard that truncates titles past roughly 60 characters and descriptions past 155 so editors cannot accidentally ship truncated snippets.
Pattern 2: SEO fields on your models
For anything editors control, attach a polymorphic seo_meta table: seoable_type, seoable_id, title, description, canonical, robots, og_image. Expose it as a relation and a trait. Your controller then asks the model for overrides and falls back to generated values. This is the piece people miss when they try to replicate Yoast — the UI matters less than having a single, queryable source of truth per record.
Pattern 3: schema as typed builders
Write small classes that return arrays: ArticleSchema::for($post), ServiceSchema::for($service), BreadcrumbSchema::from($trail). Combine them into a single @graph and render once. Typed builders are testable — you can assert that every published article emits a valid headline, author and dateModified — which is something no plugin gives you.
Package comparison, honestly
artesaos/seotools
Solid, mature, covers meta, OpenGraph, Twitter and JSON-LD with a familiar facade. Good choice if you want to stop thinking about the head tag. Downside: its JSON-LD support is basic, so you will still write your own builders for anything beyond Article and WebPage.
spatie/laravel-sitemap
Two modes: crawl your own site, or build entries from models. Use the model-driven mode. Crawling your own application on a schedule is slow, misses gated content and silently includes anything wrongly linked. Model-driven generation lets you apply exactly the same filters your routes apply — published, not noindex, not soft-deleted.
spatie/schema-org
A fluent builder over schema.org types. Worth it if your team dislikes raw arrays or you emit many different types. Skip it if you emit three types and want zero dependencies.
Nothing at all
A perfectly legitimate stack for a small app: one Seo service, one Blade partial, one artisan command that writes public/sitemap.xml, three schema builder classes. Perhaps 300 lines total, no upgrade path to manage.
Where Laravel projects actually lose rankings
In audits of Laravel applications, the recurring problems are rarely missing meta tags. They are: paginated listings that canonicalise every page to page one, filter parameters generating unlimited crawlable URLs, Inertia frontends shipping an empty body to crawlers, sitemaps generated at deploy time and never regenerated, and 302s used where 301s belong. The SEO layer is the easy part; the routing and rendering decisions are what move traffic.
Caching the SEO layer
Schema generation that hits relations on every request will show up in your response times. Cache the rendered JSON-LD string per model with a key that includes updated_at, and regenerate sitemaps on a schedule or on model events rather than per request. Metadata belongs in the fastest part of the response, because it sits in the head and blocks nothing else being parsed.
A migration checklist from WordPress
- Export existing Yoast titles and descriptions and import them into
seo_meta— do not regenerate them and lose tuned copy. - Map every old URL to a new one and implement 301s in a routes file or a redirects table, not in the web server only.
- Recreate the sitemap with the same URL set before switching DNS.
- Reproduce existing schema types so rich results do not disappear.
- Crawl staging with a desktop crawler and compare the title, canonical and status of every URL against the live site.
Done in this order, a WordPress-to-Laravel move is a rankings-neutral event. Done casually, it is the most common cause of the traffic cliff I get called in to diagnose.