jasanya / seo-library-laravel
Production-grade SEO support package for Laravel 13 Blade applications.
Package info
github.com/jasanya-tech/jasanyatech-seo-library-laravel
pkg:composer/jasanya/seo-library-laravel
Requires
- php: ^8.3
- illuminate/contracts: ^13.0
- illuminate/routing: ^13.0
- illuminate/support: ^13.0
- illuminate/view: ^13.0
Requires (Dev)
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.4
- pestphp/pest-plugin-laravel: ^4.1
This package is not auto-updated.
Last update: 2026-04-12 14:09:52 UTC
README
Reusable Laravel 13 SEO support package for Blade applications with a Blade-first API centered around:
<x-seo::meta />
The package is designed for real production usage: safe defaults, per-page overrides, JSON-LD schema support, automatic sitemap.xml, automatic robots.txt, and developer-friendly preset builders.
Architecture
Perfect SEO is split into small responsibilities:
SeoManagerstores request-scoped SEO state and exposes the public API.MetaRendererturns current SEO state into deduplicated Blade-safe tags.Schema/*contains JSON-LD schema builders with validation and omission of invalid fields.Sitemap/*handles source registration, chunking, and XML generation.Robots/RobotsRenderergenerates plain textrobots.txt.Components/Metais the Blade entry point for<x-seo::meta />.
Folder Structure
packages/seo-library-laravel/
├── composer.json
├── config/seo.php
├── resources/views/
│ ├── components/meta.blade.php
│ └── sitemap/
├── routes/seo.php
├── src/
│ ├── Components/
│ ├── Contracts/
│ ├── DTOs/
│ ├── Facades/
│ ├── Http/Controllers/
│ ├── Renderers/
│ ├── Robots/
│ ├── Schema/
│ ├── Sitemap/
│ ├── Support/
│ ├── SeoManager.php
│ └── SeoServiceProvider.php
└── tests/
Public API
use JasanyaTech\SEO\Facades\SEO; SEO::title('Home'); SEO::description('Welcome to our website'); SEO::canonical(url()->current()); SEO::robots('index,follow'); SEO::image(asset('images/og/default.jpg'), 'Default social image'); SEO::website(); SEO::organization(); SEO::breadcrumbs([ ['name' => 'Home', 'url' => route('home')], ['name' => 'Blog', 'url' => route('blog.index')], ]); SEO::article([ 'headline' => $post->title, 'description' => $post->excerpt, 'image' => $post->cover_url, 'datePublished' => $post->published_at, 'dateModified' => $post->updated_at, 'author' => $post->author_name, 'mainEntityOfPage' => route('blog.show', $post->slug), ]); SEO::forBlogPost($post); SEO::forProduct($product); SEO::forService($service); SEO::sitemap()->register('posts', fn () => Post::query() ->published() ->get(['slug', 'updated_at']) ->map(fn (Post $post) => [ 'url' => route('blog.show', $post->slug), 'lastmod' => $post->updated_at, ]));
Installation
If you extract this into its own repository:
composer require jasanya/seo-library-laravel
For a local path package in another Laravel app:
{
"repositories": [
{
"type": "path",
"url": "packages/seo-library-laravel"
}
]
}
Then publish the config:
php artisan vendor:publish --tag=seo-config
Configuration
Default config lives in config/seo.php and includes:
- site defaults
- canonical query ignore rules
- default robots
- default Open Graph image
- locale and alternate locales
- organization and website schema data
- sitemap settings
- robots settings
- environment-aware safety rules
Example:
return [ 'site' => [ 'name' => env('SEO_SITE_NAME', config('app.name')), 'url' => env('APP_URL'), 'title_separator' => '|', 'default_title' => null, 'default_description' => null, 'default_locale' => 'id_ID', 'alternate_locales' => [], ], ];
Blade Usage
Place the component inside <head>:
<head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <x-seo::meta /> </head>
You can also pass a DTO directly:
<x-seo::meta :seo="$seo" />
Controller Usage
use App\Models\Post; use JasanyaTech\SEO\Facades\SEO; public function show(Post $post) { SEO::forBlogPost($post, [ 'breadcrumbs' => [ ['name' => 'Home', 'url' => route('home')], ['name' => 'Blog', 'url' => route('blog.index')], ['name' => $post->title, 'url' => route('blog.show', $post->slug)], ], ]); return view('blog.show', compact('post')); }
Presets
Homepage
SEO::title('Home') ->description('Welcome to our website') ->canonical(route('home')) ->website() ->organization();
Blog Index
SEO::forBlogListing( title: 'Blog', description: 'Latest articles and updates', breadcrumbs: [ ['name' => 'Home', 'url' => route('home')], ['name' => 'Blog', 'url' => route('blog.index')], ], canonical: route('blog.index'), );
Blog Detail with Article Schema
SEO::forBlogPost($post, [ 'breadcrumbs' => [ ['name' => 'Home', 'url' => route('home')], ['name' => 'Blog', 'url' => route('blog.index')], ['name' => $post->title, 'url' => route('blog.show', $post->slug)], ], ]);
Product Index
SEO::forProductListing( title: 'Products', description: 'Browse our product catalog', canonical: route('products.index'), );
Product Detail
SEO::forProduct($product, [ 'breadcrumbs' => [ ['name' => 'Home', 'url' => route('home')], ['name' => 'Products', 'url' => route('products.index')], ['name' => $product->name, 'url' => route('products.show', $product->slug)], ], ]);
Service Index
SEO::forServiceListing( title: 'Services', description: 'Professional service catalog', canonical: route('services.index'), );
Service Detail
SEO::forService($service, [ 'breadcrumbs' => [ ['name' => 'Home', 'url' => route('home')], ['name' => 'Services', 'url' => route('services.index')], ['name' => $service->name, 'url' => route('services.show', $service->slug)], ], ]);
Breadcrumb Integration
SEO::breadcrumbs([ ['name' => 'Home', 'url' => route('home')], ['name' => 'Blog', 'url' => route('blog.index')], ['name' => $post->title, 'url' => route('blog.show', $post->slug)], ]);
This automatically prepares BreadcrumbList JSON-LD and stores the cleaned breadcrumb data for the current request.
Schema Usage
Supported out of the box:
WebSiteOrganizationBreadcrumbListArticleProductService
Only valid schema fields are emitted. Missing or misleading fields are omitted instead of guessed.
Sitemap Registration
Built-in routes:
/sitemap.xml/sitemaps/{source}.xml/sitemaps/{source}-{page}.xml
Register sitemap sources anywhere during bootstrapping, for example in AppServiceProvider:
use App\Models\Post; use JasanyaTech\SEO\Facades\SEO; public function boot(): void { SEO::sitemap()->register('posts', fn () => Post::query() ->whereNotNull('published_at') ->get() ->map(fn (Post $post) => [ 'url' => route('blog.show', $post->slug), 'lastmod' => $post->updated_at, 'changefreq' => 'weekly', 'priority' => 0.7, ])); }
The package:
- normalizes URLs
- skips non-indexable entries
- chunks large sources
- adds the homepage automatically if configured
robots.txt
Built-in route:
/robots.txt
Default output example:
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml
On non-production environments, the package can disallow all crawling automatically if robots.disallow_non_production is enabled.
Troubleshooting
- If your Blade output does not change, clear cached views and config.
- If Vite assets are missing in the UI, run
npm run devornpm run build. - If your sitemap is empty, verify that your registered source returns absolute public URLs.
- If JSON-LD is missing, check that the required source fields actually exist.
Testing
Package tests cover:
- title rendering
- description rendering
- canonical normalization
- robots meta rendering
- Open Graph and Twitter tags
- JSON-LD schema output
- blog/product/service presets
- sitemap index and child sitemap responses
- robots.txt output
Run the Laravel test suite:
php artisan test --compact