PHPackages                             zarbinco/zarbin-seo - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Templating &amp; Views](/categories/templating)
4. /
5. zarbinco/zarbin-seo

ActiveLibrary[Templating &amp; Views](/categories/templating)

zarbinco/zarbin-seo
===================

Lightweight model-aware SEO toolkit for Laravel.

v0.2.0(1mo ago)10MITPHPPHP ^8.2CI failing

Since Jun 29Pushed 1mo agoCompare

[ Source](https://github.com/zarbinco/zarbin-seo)[ Packagist](https://packagist.org/packages/zarbinco/zarbin-seo)[ Docs](https://github.com/zarbinco/zarbin-seo)[ RSS](/packages/zarbinco-zarbin-seo/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (7)Versions (4)Used By (0)

Zarbin SEO
==========

[](#zarbin-seo)

 **Documentation:** [English](README.md) · [فارسی](docs/fa/README.md)

Zarbin SEO is a lightweight, Laravel-native SEO toolkit for model-aware, route-aware, and multilingual metadata workflows.

It is inspired by common SEO editorial workflows and concepts popularized by tools such as Yoast SEO, but it is not a WordPress clone and is not affiliated with Yoast.

Development Status
------------------

[](#development-status)

Pre-stable / v0.2.x. The package has a tagged v0.2.0 release and is ready for early real-project usage, but the public API may still evolve before a stable v1.0 release.

Documentation
-------------

[](#documentation)

- English: [README.md](README.md)
- فارسی: [docs/fa/README.md](docs/fa/README.md)
- Integrations: [docs/integrations.md](docs/integrations.md)

Features
--------

[](#features)

- Fluent SEO manager available through `seo()` and the `ZarbinSeo` facade.
- Immutable-ish `SeoData` data object.
- Model, holder, route, array, and default SEO source resolution.
- Blade rendering for title, meta description, canonical, robots, Open Graph, Twitter/X cards, hreflang, and JSON-LD.
- Multilingual SEO with alternate language URLs, `x-default`, and missing translation strategies.
- XML sitemap, sitemap index, and robots.txt generation.
- Optional database overrides for manual SEO values.
- Optional plain Blade UI and embeddable SEO form component.
- Product and commerce schema support without ecommerce package dependencies.
- Artisan commands for install, doctor/readiness checks, source inspection, sitemap export, and robots.txt export.

Requirements
------------

[](#requirements)

- PHP `^8.2`
- Laravel/Illuminate `^10.0`, `^11.0`, `^12.0`, or `^13.0`

Installation
------------

[](#installation)

```
composer require zarbinco/zarbin-seo
```

Publish Config
--------------

[](#publish-config)

```
php artisan vendor:publish --tag=zarbin-seo-config
```

Optional resources:

```
php artisan vendor:publish --tag=zarbin-seo-migrations
php artisan vendor:publish --tag=zarbin-seo-views
php artisan vendor:publish --tag=zarbin-seo-translations
```

The migration is only needed when you enable database-backed manual overrides. Views are only needed when you want to customize the optional Blade UI or form components. Translations are only needed when you want to customize the included English/Persian UI copy or add more languages.

5-Minute Setup
--------------

[](#5-minute-setup)

1. Install the package:

```
composer require zarbinco/zarbin-seo
```

2. Publish the config:

```
php artisan vendor:publish --tag=zarbin-seo-config
```

3. Add SEO tags to your layout ``:

```
{!! seo()->render() !!}
```

4. Add one route page to `config/zarbin-seo.php`:

```
'routes' => [
    'home' => [
        'title' => 'Home',
        'description' => 'Welcome to our website.',
        'canonical' => 'https://example.com',
        'robots' => 'index, follow',
        'sitemap' => true,
    ],
],
```

5. Render that route's SEO from a controller or page:

```
seo()->route('home')->render();
```

6. Optional: enable database overrides when editors should change SEO values from the UI:

```
php artisan vendor:publish --tag=zarbin-seo-migrations
php artisan migrate
```

Which Component Should I Use?
-----------------------------

[](#which-component-should-i-use)

GoalUseI want a ready admin page.Hosted UI routes.I already have a product edit form.``I use a normal Controller + Blade form.`name="seo"` with `SeoOverridePayload::fromRequest($request)`.I use a stateful component form.`wire-model="zarbinSeo"` with `SeoFieldValues` and `SeoOverridePayload::fromLivewire()`.I use Vue, Inertia, SPA, API, or AJAX.Send a `seo` JSON object and normalize it with `SeoOverridePayload`.I only want inventory tables.`` or ``Do not place `` or `` inside another ``. Use fields-only components inside forms your application already owns.

Quick Start
-----------

[](#quick-start)

Render the current request SEO tags in your layout:

```
{!! seo()->render() !!}
```

Resolve SEO data in a controller:

```
public function show(Post $post)
{
    seo()->for($post);

    return view('posts.show', compact('post'));
}
```

Or compose SEO data directly:

```
seo()
    ->title('About Us')
    ->description('Learn more about our company.')
    ->canonical(route('about'))
    ->render();
```

Model-Aware SEO
---------------

[](#model-aware-seo)

Models can implement `Seoable` and use `HasSeo`:

```
use Illuminate\Database\Eloquent\Model;
use Zarbin\Seo\Concerns\HasSeo;
use Zarbin\Seo\Contracts\Seoable;

class Post extends Model implements Seoable
{
    use HasSeo;

    public function seoTitle(?string $locale = null): ?string
    {
        return $this->title;
    }

    public function seoDescription(?string $locale = null): ?string
    {
        return $this->excerpt;
    }

    public function seoCanonicalUrl(?string $locale = null): ?string
    {
        return route('posts.show', $this);
    }
}
```

You can also configure mappings for normal Laravel or Eloquent-like models:

```
'models' => [
    App\Models\Post::class => [
        'title' => 'title',
        'description' => ['excerpt', 'content'],
        'image' => 'cover_image_url',
        'route' => 'posts.show',
        'route_key' => 'slug',
        'type' => 'Article',
    ],
],
```

`seo()->for($post)->get()` resolves fallback defaults, configured mappings, `Seoable` values, multilingual state, optional database overrides, and optional commerce data.

Holder Pages
------------

[](#holder-pages)

Model-backed holder pages such as `ProductHolder`, `BlogHolder`, or `HomePage` work like any other `Seoable` model:

```
class ProductHolder extends Model implements Seoable
{
    use HasSeo;

    public function seoTitle(?string $locale = null): ?string
    {
        return $this->title ?: 'Products';
    }

    public function seoType(?string $locale = null): ?string
    {
        return 'CollectionPage';
    }
}
```

Route-only holder pages can use route mappings instead.

Route-Only SEO
--------------

[](#route-only-seo)

Configure SEO data for pages that do not have a model:

```
'routes' => [
    'home' => [
        'title' => 'Home',
        'description' => 'Welcome to our website.',
        'canonical' => 'https://example.com',
        'schema' => 'WebPage',
        'sitemap' => true,
    ],
],
```

Use it in controllers, layouts, or middleware:

```
seo()->route('home')->render();
```

Rendering
---------

[](#rendering)

Full render:

```
{!! seo()->render() !!}
```

Segmented render methods:

```
{!! seo()->meta() !!}
{!! seo()->openGraph() !!}
{!! seo()->twitter() !!}
{!! seo()->jsonLd() !!}
{!! seo()->alternates() !!}
```

Blade component:

```

```

Multilingual SEO
----------------

[](#multilingual-seo)

Enable localization and configure supported locales:

```
'localization' => [
    'enabled' => true,
    'locales' => ['fa', 'en'],
    'default_locale' => 'fa',
    'url_strategy' => 'prefixed_all',
    'route_parameter' => 'locale',
    'missing_translation_strategy' => 'hide',
    'generate_hreflang' => true,
    'x_default' => 'fa',
],
```

Locale URL strategies keep generated route URLs predictable:

- `default_without_prefix`: use URLs like `/about` for the default locale and `/fa/about` for other locales.
- `prefixed_all`: use URLs like `/en/about` and `/fa/about`.
- `custom`: do not infer prefixes; use `localized_urls`, `localized_routes`, model methods, or canonical URLs for special projects.

Models can implement `LocalizableSeo` when they know which languages exist:

```
use Zarbin\Seo\Contracts\LocalizableSeo;

class Post extends Model implements Seoable, LocalizableSeo
{
    use HasSeo;

    public function seoLocales(): array
    {
        return ['fa', 'en'];
    }

    public function hasSeoLocale(string $locale): bool
    {
        return filled($this->getTranslation('title', $locale));
    }

    public function seoUrlForLocale(string $locale): ?string
    {
        return route('posts.show', ['locale' => $locale, 'post' => $this->slug]);
    }
}
```

Missing translation strategies:

- `hide`: mark the current SEO data as unavailable in `extra` and skip unavailable alternates.
- `fallback`: use fallback URLs where possible and mark fallback usage in `extra`.
- `noindex`: add `noindex` to robots for unavailable current locale content.

When alternates are available, Zarbin SEO renders:

```

```

Sitemap And Robots.txt
----------------------

[](#sitemap-and-robotstxt)

Public package routes are registered by default:

```
/sitemap.xml
/sitemap_index.xml
/robots.txt

```

Projects that publish separate sitemap files per language can configure localized sitemap paths:

```
'localization' => [
    'enabled' => true,
    'locales' => ['fa', 'en'],
    'default_locale' => 'fa',
    'url_strategy' => 'prefixed_all',
    'route_parameter' => 'locale',
],

'sitemap' => [
    'base_url' => 'https://example.com',
    'content_type' => 'application/xml; charset=UTF-8',
    'localized_paths' => [
        'fa' => 'sitemap-fa.xml',
        'en' => 'sitemap-en.xml',
    ],
    'include_alternates' => false,
],
```

With that config, `/sitemap-fa.xml` renders `fa` URLs, `/sitemap-en.xml` renders `en` URLs, and `/sitemap_index.xml` lists both localized sitemap files. Localized sitemap routes return XML responses with `application/xml` content type. `localized_paths` controls the sitemap file paths. `sitemap.base_url` controls the host used for sitemap index URLs and robots.txt auto sitemap links, which is useful when the current request host differs from the public site host.

Sitemap hreflang alternates are disabled by default for cleaner sitemap XML output. Hreflang in the HTML `` remains supported through normal rendering with `seo()->alternates()` and `seo()->render()`.

Enable sitemap alternates only when you specifically want hreflang inside sitemap files:

```
'sitemap' => [
    'include_alternates' => true,
],
```

When enabled, sitemap hreflang alternates are rendered as XML-safe `xhtml:link` elements. If a local browser/server displays sitemap XML as plain text with `xhtml` alternates enabled, keep `include_alternates` disabled or rely on HTML head hreflang.

If your local server or browser displays sitemap XML as plain text, switch the HTTP response type:

```
'sitemap' => [
    'content_type' => 'text/xml; charset=UTF-8',
],
```

This affects HTTP sitemap routes only; Artisan command output remains a plain XML string. It is not a workaround for malformed XML; the sitemap XML itself must still parse cleanly.

Use `locale` or `locales` on route entries to control which localized sitemap includes each URL:

```
'routes' => [
    'products.fa' => [
        'locale' => 'fa',
        'canonical' => 'https://example.com/fa/products',
        'sitemap' => true,
    ],
    'products.en' => [
        'locale' => 'en',
        'canonical' => 'https://example.com/en/products',
        'sitemap' => true,
    ],
],
```

If a route entry has no `locale` or `locales`, it remains included as before for backward compatibility. When `robots_txt.sitemaps` is not manually configured, robots.txt points to the sitemap index.

Beginner rule of thumb:

- Route pages appear in sitemap when the route config has `sitemap => true` and a canonical or URL mapping exists.
- Model pages appear in sitemap when an explicit model sitemap source is configured, the include mode allows the item, and a canonical URL exists.
- In `overrides` mode, a model item appears only after "Include in sitemap" is checked in the SEO UI or `seo_meta.extra.sitemap.include` is saved as `true`.

Route sitemap entry:

```
'routes' => [
    'home' => [
        'title' => 'Home',
        'canonical' => 'https://example.com',
        'sitemap' => true,
        'priority' => 1.0,
        'change_frequency' => 'daily',
    ],
],
```

Model sitemap source:

```
'models' => [
    App\Models\Post::class => [
        'route' => 'posts.show',
        'route_key' => 'slug',
        'sitemap' => true,
        'sitemap_source' => App\Seo\PostSeoSitemapSource::class,
        'priority' => 0.7,
        'change_frequency' => 'weekly',
    ],
],
```

The source class should return an iterable, query, cursor, or collection of items that are allowed to be considered for sitemap output. This keeps production config cache friendly and avoids auto-crawling all model records.

Models may implement `Sitemapable` or expose matching methods:

```
public function shouldBeInSitemap(?string $locale = null): bool
{
    return $this->published;
}

public function sitemapUrl(?string $locale = null): ?string
{
    return route('posts.show', $this);
}

public function sitemapLastModified(?string $locale = null): mixed
{
    return $this->updated_at;
}
```

Commands:

```
php artisan zarbin-seo:sitemap
php artisan zarbin-seo:sitemap --locale=fa
php artisan zarbin-seo:sitemap --index
php artisan zarbin-seo:sitemap --output=public/sitemap.xml
php artisan zarbin-seo:sitemap --count

php artisan zarbin-seo:robots
php artisan zarbin-seo:robots --output=public/robots.txt
```

Commands do not write files unless `--output` is provided.

Optional Database Overrides
---------------------------

[](#optional-database-overrides)

Publish and run the migration only when you want database-backed manual SEO overrides:

```
php artisan vendor:publish --tag=zarbin-seo-migrations
php artisan migrate
```

Enable both flags:

```
'features' => [
    'database_overrides' => true,
],

'database' => [
    'enabled' => true,
],
```

Save model overrides:

```
seo()->saveOverride($post, [
    'title' => 'Custom SEO title',
    'description' => 'Custom SEO description',
    'canonical' => 'https://example.com/custom-post',
    'robots' => ['index', 'follow'],
], 'fa');
```

Save route overrides:

```
seo()->saveOverride('home', [
    'title' => 'Custom homepage title',
    'description' => 'Custom homepage description',
], 'en');
```

Eloquent models can use `HasSeoMeta`:

```
use Zarbin\Seo\Concerns\HasSeoMeta;

class Post extends Model implements Seoable
{
    use HasSeo;
    use HasSeoMeta;
}
```

The package still works normally when database overrides are disabled or the table is missing.

Optional Plain Blade UI
-----------------------

[](#optional-plain-blade-ui)

The UI is disabled by default. It edits database override records and has no CSS framework or admin panel dependency.

```
'features' => [
    'database_overrides' => true,
    'ui' => true,
],

'database' => [
    'enabled' => true,
],

'ui' => [
    'enabled' => true,
    'route_enabled' => true,
    'path' => 'admin/seo',
    'middleware' => ['web', 'auth'],
    'gate' => 'viewZarbinSeo',
    'preset' => 'unstyled',
    'form_layout' => [
        'mode' => 'responsive',
    ],
    'layout' => [
        'mode' => 'standalone',
        'view' => null,
        'section' => 'content',
        'title_section' => 'title',
    ],
    'direction' => [
        'mode' => 'auto',
        'rtl_locales' => ['fa', 'ar', 'he', 'ur', 'ku', 'ckb', 'ps', 'sd', 'yi'],
        'fallback' => 'ltr',
    ],
    'components' => [
        'global_aliases' => false,
        'alias_prefix' => 'zarbin-seo',
    ],
    'completion' => [
        'required' => ['title', 'description', 'canonical', 'robots'],
        'recommended' => ['image'],
    ],
    'inventory' => [
        'routes' => [
            'enabled' => true,
        ],
        'models' => [
            'enabled' => false,
            'default_limit' => 50,
            'max_limit' => 200,
        ],
    ],
],
```

Dedicated route UI:

```
GET /admin/seo
GET /admin/seo/routes

```

The route UI lists configured route-only pages and shows completion status. `✓` means all required SEO fields are present; `×` means one or more required fields are missing. Required and recommended fields are configurable under `ui.completion`, and recommended misses appear as warnings only. The robots field is a dropdown with common presets from `ui.robots_options`; published Blade views can still be customized.

UI strings are translated through package language files. English and Persian are included out of the box, and you can publish them with:

```
php artisan vendor:publish --tag=zarbin-seo-translations
```

The route edit screen and embeddable form show a search-result-style preview for the SEO title, canonical URL, and meta description, plus the raw generated HTML preview. The search preview is a visual approximation for editing comfort; it is not a ranking or snippet-display guarantee.

By default, the UI uses the package's standalone layout. To render it inside your application's admin layout, switch to host mode:

```
'ui' => [
    'layout' => [
        'mode' => 'host',
        'view' => 'layouts.admin',
        'section' => 'content',
        'title_section' => 'title',
    ],
],
```

Direction is locale-aware by default. `direction.mode` accepts `auto`, `rtl`, or `ltr`; in auto mode Persian, Arabic, Hebrew, Urdu, Kurdish, Central Kurdish, Pashto, Sindhi, and Yiddish render RTL by default, while English and normal LTR locales render LTR. URLs, canonical inputs, and raw HTML/code previews remain LTR for readability.

### Embeddable Blade Components

[](#embeddable-blade-components)

The preferred integration path for existing admin panels is to build your own admin page and render Zarbin SEO components inside your layout. Hosted UI routes remain available, but you do not have to adapt your admin layout to the package layout.

Full panel inside a custom admin layout:

```

```

Route inventory only:

```

```

Model inventory only:

```

```

Route edit form:

```

```

Model edit form:

```

```

Use fields-only components when your admin page already owns the form submission:

```

```

Search preview, raw HTML preview, and alerts are also available:

```

Check this SEO source before saving.
```

Publish package views to customize component markup:

```
php artisan vendor:publish --tag=zarbin-seo-views
```

Namespaced components are recommended because they avoid collisions. Optional global aliases can be enabled when you want shorter component names:

```
'ui' => [
    'components' => [
        'global_aliases' => true,
    ],
],
```

```

```

### Using SEO fields inside existing forms

[](#using-seo-fields-inside-existing-forms)

Do not place the full `` or `` inside another form. Use fields-only components inside existing host forms.

Standard Blade form:

```

    @csrf
    @method('PUT')

    Save

```

Controller:

```
use Zarbin\Seo\Support\SeoOverridePayload;

$product->fill($request->validated());
$product->save();

seo()->saveOverride($product, SeoOverridePayload::fromRequest($request), 'fa');
```

Stateful component form:

```
use Zarbin\Seo\Support\SeoFieldValues;
use Zarbin\Seo\Support\SeoOverridePayload;

public array $zarbinSeo = [];

public function mount(?int $id = null): void
{
    if ($id) {
        $this->product = Product::findOrFail($id);
        $this->zarbinSeo = app(SeoFieldValues::class)->forSource($this->product, app()->getLocale());
    }
}

public function save(): void
{
    $this->product->save();

    seo()->saveOverride(
        $this->product,
        SeoOverridePayload::normalize($this->zarbinSeo),
        app()->getLocale()
    );
}
```

```

```

For create screens, save the model first, then call `seo()->saveOverride()` for the created model instance.

### SEO analysis lights

[](#seo-analysis-lights)

The UI includes simple red/yellow/green SEO guidance indicators. They check practical basics such as title presence and length, meta description presence and length, canonical URL, and robots indexability. Optional image and schema checks can be enabled in `analysis.checks`.

These indicators are not Yoast and are not affiliated with Yoast. They are lightweight, configurable package checks for editing feedback. Analysis appears in route/model inventories, full forms, fields-only components, and the preview component.

### UI styling and design-system integration

[](#ui-styling-and-design-system-integration)

The package does not depend on any CSS framework or admin UI stack. The default UI preset is `unstyled`, which keeps output minimal and semantic. For projects that use utility-style classes, enable the optional `utility` preset:

```
'ui' => [
    'preset' => 'utility',
    'form_layout' => [
        'mode' => 'responsive',
    ],
],
```

The package only emits class strings. Styling remains owned by the host application. For a fully project-owned class map, use `custom`:

```
'ui' => [
    'preset' => 'custom',
    'classes' => [
        'field_input' => 'project-field project-field-input',
        'button_primary' => 'project-button project-button-primary',
        'card' => 'project-card',
    ],
],
```

Any preset class can be overridden key by key through `ui.classes`:

```
'ui' => [
    'preset' => 'utility',
    'classes' => [
        'field_input' => 'project-field project-field-input',
        'button_primary' => 'project-button project-button-primary',
    ],
],
```

```
php artisan vendor:publish --tag=zarbin-seo-views
```

Field layout can be tuned without editing views. `full_width_fields` span the full grid on tablet/desktop, while `compact_fields` remain normal grid columns:

```
'ui' => [
    'form_layout' => [
        'mode' => 'responsive', // responsive | stacked
        'full_width_fields' => ['title', 'description', 'canonical', 'extra'],
        'compact_fields' => ['robots', 'schema_type', 'sitemap_include', 'sitemap_priority', 'sitemap_change_frequency'],
    ],
],
```

RTL/LTR detection remains automatic. Search-preview text follows the selected locale direction, while canonical URLs and raw HTML previews stay LTR.

Model and holder inventory is opt-in. The package does not crawl every model class and does not run `Model::query()->get()` automatically. To list products, posts, pages, or holder-like models in the dedicated UI, enable model inventory globally and provide an explicit source for each configured model:

```
'ui' => [
    'inventory' => [
        'models' => [
            'enabled' => true,
            'default_limit' => 50,
            'max_limit' => 200,
        ],
    ],
],

'models' => [
    App\Models\Product::class => [
        'route' => 'products.show',
        'route_key' => 'slug',
        'title' => 'title',
        'description' => 'description',
        'ui' => [
            'enabled' => true,
            'label' => 'Products',
            'source' => fn () => App\Models\Product::query()->latest()->limit(50)->get(),
            'key' => 'id',
            'display' => ['title', 'name', 'slug'],
        ],
    ],
],
```

Model edit screens use the same database override system as route edit screens, so saving a model item stores an override for that specific model instance and optional locale.

### Model sitemap controls

[](#model-sitemap-controls)

SEO overrides do not automatically add model items to sitemap output. Sitemap inclusion is explicit:

1. Configure a model sitemap source.
2. Choose an include mode: `all`, `overrides`, or `none`.
3. For `overrides`, check "Include in sitemap" in the SEO UI or save `seo_meta.extra.sitemap.include = true`.

The sitemap controls in forms and fields-only components store values inside `seo_meta.extra`:

```
[
    'sitemap' => [
        'include' => true,
        'priority' => 0.7,
        'change_frequency' => 'weekly',
    ],
]
```

Example model sitemap config:

```
'models' => [
    App\Models\Product::class => [
        'route' => 'products.show',
        'route_key' => 'slug',
        'title' => 'title',
        'description' => 'description',
        'sitemap' => [
            'enabled' => true,
            'include' => 'overrides', // all | overrides | none
            'source' => App\Seo\ProductSeoSitemapSource::class,
            'default_priority' => 0.7,
            'default_change_frequency' => 'weekly',
        ],
    ],
],
```

`sitemap.source` can be a class-string provider, callable, iterable, query, or cursor-like source. Class-string providers are recommended for production config caching. The package never falls back to crawling all records with `Model::query()->get()`.

### Localized canonical URL mapping

[](#localized-canonical-url-mapping)

Sitemap generation cannot always guess a project's public URL structure. For prefixed-locale applications, configure explicit URL mapping so product URLs become `/fa/products/slug` instead of a legacy `?locale=fa` fallback.

```
'url_generation' => [
    'base_url' => 'https://example.com',
    'locale_strategy' => null, // inherits localization.url_strategy
],

'models' => [
    App\Models\Product::class => [
        'title' => 'title',
        'description' => 'description',
        'url' => [
            'strategy' => 'pattern',
            'pattern' => '/{locale}/products/{slug}',
            'fields' => [
                'slug' => 'slug',
            ],
        ],
        'sitemap' => [
            'enabled' => true,
            'include' => 'overrides',
            'source' => App\Seo\ProductSeoSitemapSource::class,
        ],
    ],
],
```

Route-only pages can use the same mapping:

```
'routes' => [
    'about' => [
        'title' => 'About',
        'sitemap' => true,
        'url' => [
            'strategy' => 'pattern',
            'pattern' => '/{locale}/about',
        ],
    ],
],
```

Supported URL mapping strategies are `pattern`, `route`, `localized_routes`, and `class`. Database override canonicals still win when explicitly saved, and existing `seoCanonicalUrl()` / route fallback behavior remains available for backward compatibility.

If a canonical URL is explicitly saved in `seo_meta`, sitemap generation, admin previews, and rendered canonical tags keep using that saved override until you edit it in the SEO UI or delete/re-save the override. In prefixed-locale projects, saved canonical values should match the real public URL shape, such as `/fa/products/slug`.

Run the doctor to catch accidental query-locale URLs produced by current URL generation in prefixed-locale projects:

```
php artisan zarbin-seo:doctor
```

### Published view compatibility

[](#published-view-compatibility)

Published views under `resources/views/vendor/zarbin-seo` override package view updates. If an old published fields view is missing current classes or `wire:model` bindings, embedded fields can render without styling or fail to update host form state.

Critical package views include compatibility markers. The doctor warns with `ui.published_views.stale` when a published critical view is missing or has an old marker. Fix by republishing or manually merging:

```
php artisan vendor:publish --tag=zarbin-seo-views --force
```

### Troubleshooting

[](#troubleshooting)

ProblemFirst checksFields do not update in a stateful form.Confirm rendered inputs include `wire:model`, then run `php artisan zarbin-seo:doctor` to check for stale published views.Inputs have no visible styling.Republish or merge `zarbin-seo-views`, or configure `ui.classes` for your design system.A product is not in sitemap.Check the model sitemap source, include mode, `seo_meta.extra.sitemap.include`, and canonical URL.URLs look like `?locale=fa`.Add explicit URL pattern mapping such as `/{locale}/products/{slug}`.Sitemap or robots routes seem ignored.Static files like `public/sitemap.xml` or `public/robots.txt` can be served before Laravel routes; rename or remove them if package routes should handle those URLs.The original embeddable form component remains available for custom save flows:

```

```

Product / Commerce Schema
-------------------------

[](#product--commerce-schema)

Commerce support is disabled by default and does not depend on WooCommerce, Cashier, Bagisto, Aimeos, Vanilo, Stripe, Paddle, or any ecommerce package.

```
'features' => [
    'commerce' => true,
],

'commerce' => [
    'enabled' => true,
    'offer' => [
        'enabled' => 'auto',
        'require_price' => true,
    ],
    'default_currency' => 'IRR',
    'currency_per_locale' => [
        'fa' => 'IRR',
        'en' => 'USD',
    ],
],
```

Offer generation is optional. The default `auto` mode builds an `Offer` only when real priced offer data exists, so corporate/catalog product pages can render valid Product schema without pretending to sell online. HTML/head SEO and commerce schema do not require an ecommerce package.

Model mapping:

```
'models' => [
    App\Models\Product::class => [
        'route' => 'products.show',
        'route_key' => 'slug',
        'type' => 'Product',
        'commerce' => [
            'enabled' => true,
            'name' => 'name',
            'description' => ['short_description', 'description'],
            'image' => 'image_url',
            'price' => 'price',
            'currency' => 'currency',
            'sku' => 'sku',
            'brand' => 'brand.name',
            'availability' => 'stock_status',
            'condition' => 'condition',
        ],
    ],
],
```

Commerce mappings can read nested relations and locale-aware collections. This is useful when price or copy lives on translations, discounts, active offers, variants, or price rows.

Catalog product without Offer:

```
'commerce' => [
    'enabled' => true,
    'name' => ['translations[locale={locale}].title', 'title'],
    'description' => ['translations[locale={locale}].description', 'description'],
    'brand' => 'brand.name',
],
```

Ecommerce product with translated price:

```
'commerce' => [
    'enabled' => true,
    'name' => ['translations[locale={locale}].title', 'title'],
    'price' => ['translations[locale={locale}].price', 'activeOffer.price', 'discount.price'],
    'currency' => 'literal:IRR',
],
```

Relation filter form:

```
'price' => [
    'relation' => 'translations',
    'where' => ['locale' => '{locale}'],
    'value' => 'price',
],
```

Fallback arrays return the first non-empty value and preserve `0` / `"0"`. Callables are also supported for project-specific lookups.

Contract-based product data:

```
use Zarbin\Seo\Contracts\CommerceSeo;
use Zarbin\Seo\Data\CommerceData;

class Product extends Model implements Seoable, CommerceSeo
{
    use HasSeo;

    public function toCommerceData(?string $locale = null): CommerceData|array|null
    {
        return CommerceData::make([
            'name' => $this->name,
            'price' => $this->price,
            'currency' => 'IRR',
            'availability' => $this->stock > 0 ? 'in_stock' : 'out_of_stock',
            'brand' => $this->brand?->name,
            'sku' => $this->sku,
        ]);
    }
}
```

Fluent commerce data:

```
seo()
    ->title('Product title')
    ->commerce([
        'price' => 120000,
        'currency' => 'IRR',
        'availability' => 'in_stock',
    ])
    ->render();
```

Product holder and listing pages should usually remain `CollectionPage`. ItemList schema may be added in a future phase.

Artisan Commands
----------------

[](#artisan-commands)

Install:

```
php artisan zarbin-seo:install
php artisan zarbin-seo:install --all
php artisan zarbin-seo:install --migrations --run-migrations
```

Doctor/readiness checks:

```
php artisan zarbin-seo:doctor
php artisan zarbin-seo:doctor --strict
php artisan zarbin-seo:doctor --json
```

### Production readiness / Doctor

[](#production-readiness--doctor)

Run the doctor before deploying or tagging a release:

```
php artisan zarbin-seo:doctor
php artisan zarbin-seo:doctor --strict
php artisan zarbin-seo:doctor --json
```

Normal mode reports warnings but exits successfully. `--strict` escalates fail-level production risks, and `--json` returns stable keyed result entries for CI or release checklists.

The doctor checks for public `sitemap.xml` / `robots.txt` files that can shadow package routes, database override table readiness, UI route middleware/gate protection, config-cache unsafe closures, sitemap base URL and localized path issues, model inventory source safety, and published config/views/translations that may need manual merging after upgrades.

Inspect resolved SEO data:

```
php artisan zarbin-seo:check
php artisan zarbin-seo:check --route=home
php artisan zarbin-seo:check --route=home --locale=fa --render
php artisan zarbin-seo:check --model="App\Models\Post" --id=1 --json
```

Export sitemap or robots output:

```
php artisan zarbin-seo:sitemap --output=public/sitemap.xml
php artisan zarbin-seo:robots --output=public/robots.txt
```

Commands are safe by default: no model crawling unless explicitly requested, no migrations unless `--run-migrations` is provided, and no file writes unless `--output` is provided.

Configuration Reference
-----------------------

[](#configuration-reference)

Important config areas:

- `defaults`: fallback title, description, image, separator, robots, and description limit.
- `features`: toggle Open Graph, Twitter, schema, sitemap, robots.txt, alternates, database overrides, UI, and commerce.
- `localization`: locales, default locale, URL strategy, route parameter, missing translation strategy, hreflang, and `x-default`.
- `sitemap`: public route, default/index paths, localized paths, defaults, alternates, and cache placeholders.
- `robots_txt`: public route, user-agent, allow/disallow, and sitemap lines.
- `database`: optional override table/model settings.
- `ui`: optional Blade UI route, middleware, gate, and preview settings.
- `commerce`: optional Offer generation, default currency, locale currencies, availability map, and condition map.
- `models`: model and holder mappings.
- `routes`: route-only SEO mappings.

Testing
-------

[](#testing)

```
composer test
composer format:test
```

Consumer App Smoke Test
-----------------------

[](#consumer-app-smoke-test)

Before releasing a new tag, you can run a real Laravel consumer-app smoke test:

```
php scripts/e2e-consumer-app.php
```

The script creates a temporary Laravel app, installs this package through a Composer path repository, and verifies package discovery, publish tags, commands, sitemap/robots routes, and Blade rendering. See [docs/e2e.md](docs/e2e.md).

Hardening / Bulletproof Tests
-----------------------------

[](#hardening--bulletproof-tests)

The test suite includes bulletproof coverage for broken config, disabled features, missing database tables, invalid routes, malformed localization, UI/database mismatch, commerce edge cases, and rendering safety. See [docs/hardening.md](docs/hardening.md).

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md).

Security
--------

[](#security)

If you discover a security issue, please open a private/security report through GitHub if enabled, or contact the maintainer through the repository.

Credits
-------

[](#credits)

Zarbin / zarbinco

License
-------

[](#license)

The MIT License (MIT). See [LICENSE.md](LICENSE.md).

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance90

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

3

Last Release

49d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b3f772fb52747f8a5886d060e6da693a7aef6952200c2227d49c68ff4a1b4bfe?d=identicon)[zarbinco](/maintainers/zarbinco)

---

Top Contributors

[![Misagh90](https://avatars.githubusercontent.com/u/6697222?v=4)](https://github.com/Misagh90 "Misagh90 (39 commits)")

---

Tags

bladecomponentsjson-ldlaravellaravel-seometadatamultilingualopengraphphprobotsseositemaplaravelschemaJSON-LDSitemapseometatwitter cardrobotsopengraph

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/zarbinco-zarbin-seo/health.svg)

```
[![Health](https://phpackages.com/badges/zarbinco-zarbin-seo/health.svg)](https://phpackages.com/packages/zarbinco-zarbin-seo)
```

###  Alternatives

[spatie/laravel-medialibrary

Associate files with Eloquent models

6.2k45.4M713](/packages/spatie-laravel-medialibrary)[spatie/laravel-health

Monitor the health of a Laravel application

88212.7M190](/packages/spatie-laravel-health)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5226.7k](/packages/simplestats-io-laravel-client)[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)[pressbooks/pressbooks

Pressbooks is an open source book publishing tool built on a WordPress multisite platform. Pressbooks outputs books in multiple formats, including PDF, EPUB, web, and a variety of XML flavours, using a theming/templating system, driven by CSS.

45844.8k1](/packages/pressbooks-pressbooks)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
