kfoobar / laravel-seo-tools
Breadcrumbs, robots.txt, and sitemaps for Laravel — a companion to laravel/head
Requires
- php: ^8.3
- illuminate/contracts: ^13.17
- illuminate/routing: ^13.17
- illuminate/support: ^13.17
- illuminate/view: ^13.17
- laravel/head: ^0.2
Requires (Dev)
- laravel/pint: ^1.28
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Breadcrumbs, robots.txt, and XML sitemaps for Laravel. Meta tags, Open Graph, and JSON-LD stay in laravel/head.
Installation
composer require kfoobar/laravel-seo-tools
The package requires PHP 8.3+, Laravel 13.17+, and laravel/head.
Publish the config (optional):
php artisan vendor:publish --tag=seo-config
Publish the breadcrumb view (optional):
php artisan vendor:publish --tag=seo-views
Layout
<head> @head </head> <body> <x-seo::breadcrumbs />
/robots.txt and /sitemap.xml are registered automatically.
Breadcrumbs
Add crumbs in a controller (or anywhere before @head renders):
use KFoobar\LaravelSeoTools\Facades\Breadcrumbs; Breadcrumbs::add('Home', route('home')) ->add('Blog', route('posts.index')) ->add($post->title);
The last crumb is the current page: it is not linked, and it gets aria-current="page". The same trail is pushed to Head::schema(Schema::breadcrumbs()) so JSON-LD is rendered by @head without duplicating data.
If the trail is empty, neither the nav nor the schema is rendered.
robots.txt
Configured in config/seo.php, not per request:
'robots' => [ 'enabled' => true, 'user_agents' => [ '*' => [ 'allow' => ['/'], 'disallow' => ['/admin', '/horizon', '/telescope'], ], ], 'sitemap' => true, // Sitemap: {app.url}/sitemap.xml ],
This is the site-wide crawl file. Page-level meta robots still belongs to Laravel Head.
Sitemap
No crawler. URLs and models are read from config on each request, so the list stays the same under PHP-FPM and Octane. Each entry is a URL plus optional lastmod. Google ignores changefreq and priority.
'sitemap' => [ 'enabled' => true, 'route' => '/sitemap.xml', 'cache' => 3600, // seconds; false to disable. The cache key includes model counts and lastmod. 'urls' => [ '/', ['loc' => '/contact', 'lastmod' => '2026-03-01'], ], 'models' => [ App\Models\Post::class, ], ],
use KFoobar\LaravelSeoTools\Contracts\Sitemapable; class Post extends Model implements Sitemapable { public function toSitemapTag(): string|array { return [ 'loc' => route('posts.show', $this), 'lastmod' => $this->updated_at, ]; } }
toSitemapTag() may also return a URL string. Models are read with lazy(), so the full table is not loaded at once.
Set enabled to false before routes are cached if the application serves its own /sitemap.xml or /robots.txt. Rebuild the route cache after changing that flag.
Testing
composer test