PHPackages                             three\_oh\_eight/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. [Utility &amp; Helpers](/categories/utility)
4. /
5. three\_oh\_eight/seo

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

three\_oh\_eight/seo
====================

Lean, Laravel-native SEO tag management

v0.2.1(2w ago)057↓64.1%MITPHPPHP ^8.5

Since Mar 25Pushed 2w agoCompare

[ Source](https://github.com/Three-Oh-Eight/seo)[ Packagist](https://packagist.org/packages/three_oh_eight/seo)[ RSS](/packages/three-oh-eight-seo/feed)WikiDiscussions main Synced 3d ago

READMEChangelogDependencies (10)Versions (3)Used By (0)

three\_oh\_eight/seo
====================

[](#three_oh_eightseo)

Lean, Laravel-native SEO tag management. One facade, one config file, zero bloat.

Built for Laravel 12+ and PHP 8.5+. Octane-safe (scoped binding).

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

[](#installation)

```
composer require three_oh_eight/seo
```

The service provider and `Seo` facade are auto-discovered.

Publish the config:

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

Quick start
-----------

[](#quick-start)

Set SEO data anywhere (controller, middleware, Livewire component):

```
use ThreeOhEight\Seo\Facades\Seo;

Seo::title('Dashboard')
    ->description('Your account overview')
    ->image('https://example.com/og.jpg');
```

Render in your layout:

```

    @seo

```

That's it. The `@seo` directive renders ``, meta description, canonical, robots, Open Graph, Twitter Card, and JSON-LD tags.

Configuration
-------------

[](#configuration)

All defaults live in `config/seo.php`:

```
return [
    'site_name'      => env('APP_NAME', 'My App'),
    'separator'      => ' - ',
    'title'          => null,          // default page title (null = site name only)
    'description'    => null,          // default meta description
    'auto_canonical' => true,          // auto-generate from url()->current()
    'robots'         => null,          // null = don't render robots meta
    'og_type'        => 'website',
    'og_image'       => null,          // fallback OG image
    'twitter_card'   => 'summary_large_image',
    'twitter_image'  => null,          // fallback Twitter image
    'twitter_site'   => null,          // @handle
];
```

API reference
-------------

[](#api-reference)

### Basic tags

[](#basic-tags)

```
Seo::title('About Us');                     // About Us - Site Name
Seo::description('Learn about us');         //
Seo::canonical('https://example.com/about');//
Seo::image('https://example.com/og.jpg');   // og:image + twitter:image
Seo::robots('noindex, follow');             //
Seo::noindex();                             // shortcut: "noindex, nofollow"
Seo::meta('author', 'Christoph');           // arbitrary  tags
```

### Pagination

[](#pagination)

```
Seo::prev('https://example.com?page=1');
Seo::next('https://example.com?page=3');

// Or from a paginator:
Seo::paginate($posts);  // auto-sets prev/next from LengthAwarePaginator
```

### Open Graph &amp; Twitter overrides

[](#open-graph--twitter-overrides)

Override platform-specific titles/descriptions when they should differ from the `` tag:

```
Seo::og()->title('Custom OG title');
Seo::og()->description('Custom OG description');

Seo::twitter()->title('Custom Twitter title');
Seo::twitter()->description('Custom Twitter description');
```

Both proxies return the `Seo` instance, so you can chain:

```
Seo::title('Page')
    ->og()->title('OG override')
    ->twitter()->title('Twitter override')
    ->description('Shared description');
```

Set the Open Graph locale (and any alternates) for multilingual pages:

```
Seo::og()->locale('en_US')->og()->alternateLocale('nl_NL');

// alternateLocale also accepts an array, and dedupes:
Seo::og()->alternateLocale(['nl_NL', 'de_DE']);
```

Renders:

```

```

`og:locale` is emitted only when set; one `og:locale:alternate` tag is emitted per alternate.

### JSON-LD

[](#json-ld)

```
Seo::jsonLd('Organization')
    ->title('Acme Corp')
    ->description('We make things')
    ->value('url', 'https://acme.com')
    ->value('logo', 'https://acme.com/logo.png');
```

Multiple blocks render as a `@graph` array:

```
Seo::jsonLd('WebSite')->title('Acme')->value('url', 'https://acme.com');
Seo::jsonLd('Organization')->title('Acme Corp');
// Outputs: {"@context":"https://schema.org","@graph":[...]}
```

Nested blocks:

```
$address = JsonLdBlock::make('PostalAddress')->value('addressLocality', 'Rotterdam');
Seo::jsonLd('Organization')->title('Acme')->value('address', $address);
```

### Breadcrumbs

[](#breadcrumbs)

```
Seo::breadcrumbs([
    'Home'     => '/',
    'Products' => '/products',
    'Widget'   => null,  // null = current page (no URL in output)
]);
```

Renders a `BreadcrumbList` JSON-LD block with auto-incrementing positions.

### Seoable models

[](#seoable-models)

Implement the `Seoable` interface on your models:

```
use ThreeOhEight\Seo\Contracts\Seoable;
use ThreeOhEight\Seo\Seo;

class Post extends Model implements Seoable
{
    public function toSeo(Seo $seo): void
    {
        $seo->title($this->title)
            ->description($this->excerpt)
            ->image($this->cover_image);
    }
}
```

Then in a controller:

```
Seo::from($post);
```

Or use the `HasSeo` trait for convention-based mapping (pulls from `meta_title`/`title` and `meta_description`/`description`/`excerpt`):

```
use ThreeOhEight\Seo\Concerns\HasSeo;
use ThreeOhEight\Seo\Contracts\Seoable;

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

### Livewire integration

[](#livewire-integration)

Add `WithSeo` to your Livewire component:

```
use ThreeOhEight\Seo\Concerns\WithSeo;

class ShowPost extends Component
{
    use WithSeo;

    public Post $post; // auto-detected if Post implements Seoable

    // Or define a custom seo() method:
    public function seo(Seo $seo): void
    {
        $seo->title($this->post->title)
            ->description($this->post->excerpt);
    }
}
```

The trait hooks into `rendering()` and applies SEO data automatically. If a `seo()` method exists, it takes priority over auto-detection of Seoable properties.

### Macros

[](#macros)

The `Seo` class uses Laravel's `Macroable` trait:

```
Seo::macro('article', function (string $author, string $published) {
    return $this->meta('article:author', $author)
                ->meta('article:published_time', $published);
});

Seo::article('Christoph', '2026-01-01');
```

Rendering
---------

[](#rendering)

### Blade directive (recommended)

[](#blade-directive-recommended)

```

    @seo

```

### Blade components

[](#blade-components)

```
          {{-- all sections --}}
          {{-- title, description, canonical, robots, prev/next, custom meta --}}
     {{-- og:* tags --}}
       {{-- twitter:* tags --}}
       {{-- JSON-LD script --}}
```

### Programmatic

[](#programmatic)

```
$html = Seo::render();          // SeoOutput (Htmlable + Stringable)
$html = Seo::renderMeta();      // meta section only
$html = Seo::renderOpenGraph();
$html = Seo::renderTwitter();
$html = Seo::renderJsonLd();
```

Fallback cascade
----------------

[](#fallback-cascade)

Tags resolve in this order:

TagResolution```title()` &gt; config `title` &gt; site name only`meta description``description()` &gt; config `description` &gt; omitted`canonical``canonical()` &gt; `url()->current()` (if `auto_canonical`) &gt; omitted`robots``robots()`/`noindex()` &gt; config `robots` &gt; omitted`og:title``og()->title()` &gt; formatted page title &gt; site name`og:description``og()->description()` &gt; `description()` &gt; config `description` &gt; omitted`og:image``image()` &gt; config `og_image` &gt; omitted`og:locale``og()->locale()` &gt; omitted`og:locale:alternate``og()->alternateLocale()` (one tag per entry) &gt; omitted`twitter:title``twitter()->title()` &gt; formatted page title &gt; site name`twitter:description``twitter()->description()` &gt; `description()` &gt; config `description` &gt; omitted`twitter:image``image()` &gt; config `twitter_image` &gt; omittedTitle format
------------

[](#title-format)

Titles are formatted as `{page title}{separator}{site name}`:

- `Seo::title('Dashboard')` renders `Dashboard - My App`
- No page title renders `My App`

OG and Twitter titles use the same formatted title unless overridden via their proxies.

Testing
-------

[](#testing)

```
composer test
# or
./vendor/bin/pest
```

License
-------

[](#license)

MIT

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance96

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

 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 ~82 days

Total

2

Last Release

20d ago

### Community

Maintainers

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

---

Top Contributors

[![webpatser](https://avatars.githubusercontent.com/u/25720?v=4)](https://github.com/webpatser "webpatser (5 commits)")

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/three-oh-eight-seo/health.svg)

```
[![Health](https://phpackages.com/badges/three-oh-eight-seo/health.svg)](https://phpackages.com/packages/three-oh-eight-seo)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3355.3M346](/packages/psalm-plugin-laravel)[livewire/flux

The official UI component library for Livewire.

9527.8M128](/packages/livewire-flux)[moonshine/moonshine

Laravel administration panel

1.3k253.1k81](/packages/moonshine-moonshine)[tallstackui/tallstackui

TallStackUI is a powerful suite of Blade components that elevate your workflow of Livewire applications.

725173.6k14](/packages/tallstackui-tallstackui)[webwizo/laravel-shortcodes

Wordpress like shortcodes for Laravel 11, 12 and 13

217700.9k8](/packages/webwizo-laravel-shortcodes)[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.

45444.2k1](/packages/pressbooks-pressbooks)

PHPackages © 2026

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