jasanya/seo-library-laravel

Production-grade SEO support package for Laravel 13 Blade applications.

Maintainers

Package info

github.com/jasanya-tech/jasanyatech-seo-library-laravel

pkg:composer/jasanya/seo-library-laravel

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

1.0.0 2026-03-28 15:24 UTC

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:

  • SeoManager stores request-scoped SEO state and exposes the public API.
  • MetaRenderer turns 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/RobotsRenderer generates plain text robots.txt.
  • Components/Meta is 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:

  • WebSite
  • Organization
  • BreadcrumbList
  • Article
  • Product
  • Service

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 dev or npm 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