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. three\_oh\_eight/seo

ActiveLibrary

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

Lean, Laravel-native SEO tag management

v0.1.1(1mo ago)04↑2150%MITPHPPHP ^8.5

Since Mar 25Pushed 1mo 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 1mo ago

READMEChangelogDependencies (5)Versions (2)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');
```

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

37

—

LowBetter than 83% of packages

Maintenance90

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity41

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

Unknown

Total

1

Last Release

57d 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 (3 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

[livewire/blaze

A tool for optimizing Blade component performance by folding them into parent templates

688221.3k17](/packages/livewire-blaze)[glhd/aire

Modern Laravel form builder. Remembers old input, retrieves error messages and comes with beautiful Tailwind-based markup out of the box.

545265.3k7](/packages/glhd-aire)[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.

44643.1k1](/packages/pressbooks-pressbooks)[konekt/html

HTML and Form Builders for the Laravel Framework

25403.2k5](/packages/konekt-html)[bjuppa/laravel-blog

Add blog functionality to your Laravel project

483.3k2](/packages/bjuppa-laravel-blog)[evanschleret/lara-mjml

Just a service provider for Spatie's MJML wrapper

1722.0k](/packages/evanschleret-lara-mjml)

PHPackages © 2026

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