承接 zarbinco/zarbin-seo 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

zarbinco/zarbin-seo

Composer 安装命令:

composer require zarbinco/zarbin-seo

包简介

Lightweight model-aware SEO toolkit for Laravel.

README 文档

README

Documentation: English · فارسی

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

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

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

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

Installation

composer require zarbinco/zarbin-seo

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

  1. Install the package:
composer require zarbinco/zarbin-seo
  1. Publish the config:
php artisan vendor:publish --tag=zarbin-seo-config
  1. Add SEO tags to your layout <head>:
{!! seo()->render() !!}
  1. 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,
    ],
],
  1. Render that route's SEO from a controller or page:
seo()->route('home')->render();
  1. 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?

Goal Use
I want a ready admin page. Hosted UI routes.
I already have a product edit form. <x-zarbin-seo::model-fields />
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. <x-zarbin-seo::routes /> or <x-zarbin-seo::models />

Do not place <x-zarbin-seo::model-form> or <x-zarbin-seo::route-form> inside another <form>. Use fields-only components inside forms your application already owns.

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

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

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

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

Full render:

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

Segmented render methods:

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

Blade component:

<x-zarbin-seo::meta />
<x-zarbin-seo::meta :source="$post" locale="fa" />

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:

<link rel="alternate" hreflang="fa" href="https://example.com/fa/posts/hello">
<link rel="alternate" hreflang="en" href="https://example.com/en/posts/hello">
<link rel="alternate" hreflang="x-default" href="https://example.com/fa/posts/hello">

Sitemap And Robots.txt

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 <head> 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

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

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

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:

<x-admin-layout>
    <x-zarbin-seo::panel locale="fa" />
</x-admin-layout>

Route inventory only:

<x-zarbin-seo::routes locale="fa" />

Model inventory only:

<x-zarbin-seo::models locale="fa" />

Route edit form:

<x-zarbin-seo::route-form route="catalog.products.fa" locale="fa" />

Model edit form:

<x-zarbin-seo::model-form :source="$product" locale="fa" />

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

<x-zarbin-seo::model-fields :source="$product" locale="fa" name="seo" />
<x-zarbin-seo::route-fields route="catalog.products.fa" locale="fa" />
<x-zarbin-seo::fields :source="$product" locale="fa" wire-model="zarbinSeo" />

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

<x-zarbin-seo::preview :data="$seoData" />
<x-zarbin-seo::analysis :data="$seoData" />
<x-zarbin-seo::alert type="warning">Check this SEO source before saving.</x-zarbin-seo::alert>

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,
    ],
],
<x-zarbin-seo-panel />

Using SEO fields inside existing forms

Do not place the full <x-zarbin-seo::model-form> or <x-zarbin-seo::route-form> inside another form. Use fields-only components inside existing host forms.

Standard Blade form:

<form method="POST" action="{{ route('admin.products.update', $product) }}">
    @csrf
    @method('PUT')

    <!-- Product fields... -->

    <x-zarbin-seo::model-fields :source="$product" locale="fa" name="seo" />

    <button type="submit">Save</button>
</form>

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()
    );
}
<x-project-form submit="save">
    <!-- Product fields... -->

    <x-zarbin-seo::model-fields :source="$product ?? null" locale="fa" wire-model="zarbinSeo" />
</x-project-form>

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

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

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

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

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 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

Problem First checks
Fields 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:

<x-zarbin-seo::form :source="$post" locale="fa" />
<x-zarbin-seo::form source="home" locale="en" action="{{ route('admin.seo.save') }}" standalone />

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

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

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

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

composer test
composer format: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.

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.

Changelog

See CHANGELOG.md.

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

Zarbin / zarbinco

License

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

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 2
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-04

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固