Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Advanced Seo Laravel Package

aerni/advanced-seo

A Laravel package for advanced SEO management: define meta tags, Open Graph and Twitter Card data, canonical URLs, and robots directives in code. It keeps SEO configuration centralized, consistent, and easy to customize across your application.

View on GitHub
Deep Wiki
Context7

Getting Started

aerni/advanced-seo is a Statamic addon, not a generic Laravel package. Do not try to install it into a plain Laravel app — it requires Statamic 6, Laravel 12, and PHP 8.4 (per its composer.json). Install with:

composer require aerni/advanced-seo

Then run the install command / publish step the docs describe (config + any Eloquent migrations if you use the Eloquent driver), and clear caches (php artisan cache:clear, statamic:stache:clear) before touching the Control Panel, or you will chase ghosts on first boot.

Where to look first

  1. The official docs: https://advanced-seo.michaelaerni.ch — this is the authoritative source; the README is thin.
  2. src/Facades/Seo.php in vendor/aerni/advanced-seo — the facade is the front door to the whole domain. Everything you need programmatically is reachable from it.
  3. The SEO tab that appears in the Control Panel on Sites, Collections, and Taxonomies after install. Editors configure defaults there; that is the surface you will be extending and debugging.

First use case: render meta tags in your layout.

Open your Antlers layout (resources/views/layout.antlers.html or your Blade equivalent) and output the computed seo variable inside <head>:

<head>
    {{ seo }}
</head>

That single variable is the product. It is assembled by a CascadeComposer from site defaults → collection/taxonomy defaults → entry overrides, and it already contains the title, canonical, OG/Twitter tags, robots directives, and JSON-LD schema. Get this working on one collection before you configure anything else.

The second thing you will do is open the CP, go to Sites → your site → SEO, and fill in the site-level defaults (site name, title template, social image). Everything else layers on top of that.

Implementation Patterns

Respect the merge order, don't fight it

The layering is fixed: site defaults → collection/taxonomy defaults → entry/term overrides. In practice this means:

  • Put site-wide fallbacks (site name, default OG image, title suffix pattern) on the site SEO set.
  • Put per-section rules (e.g. "blog posts use Article schema", "products get noindex when out of stock") on the collection SEO set.
  • Leave entry-level overrides to editors, and only programmatically write them when you genuinely need per-entry control.

When a field is empty at one level, it falls through to the next. That fallthrough is the feature — don't duplicate site defaults onto every collection "just to be safe", or editors lose the ability to change one thing in one place.

Reach data through the Seo facade

Never instantiate SeoSet, SeoSetConfig, or SeoSetLocalization yourself in application code. The facade gives you:

use Aerni\AdvancedSeo\Facades\Seo;

$seoSet = Seo::find('collections::pages');

$config        = $seoSet->config();        // SeoSetConfig
$localization  = $seoSet->localization(); // SeoSetLocalization for current site

The handle convention is {type}::{handle} — collections::pages, taxonomies::tags, sites::default. Only reach for the SeoConfig / SeoLocalization facades when you genuinely need the low-level repositories (bulk reads, custom tooling), not in controllers, middleware, or templates.

Build a proper Context when you are outside the request lifecycle

Anything that resolves SEO for a model — a queued job regenerating sitemaps, a CLI command, an API endpoint — should build a Context explicitly rather than relying on the current request:

use Aerni\AdvancedSeo\Context;

$context = Context::from($entry); // parent, type, handle, scope, site

This is the difference between "works in the browser, breaks in the queue worker" and a reliable integration. If you write an Artisan command that touches SEO, Context::from($entry) is your first line.

Customizing the head output

The {{ seo }} variable is a ViewCascade. If you need to inject anything (a custom <meta> tag, an analytics snippet, a per-page JSON-LD block), the supported extension point is augmenting the view data before CascadeComposer builds the cascade, not string-manipulating the rendered output. Rendering {{ seo }} and doing str_replace on it is fragile — the tag order and whitespace are not a contract.

For schema, the package wraps spatie/schema-org v3. If you need a custom type (e.g. Recipe, Event, or a nested Organization reference), build the Spatie\SchemaOrg object and attach it via the cascade data; do not hand-write a second <script type="application/ld+json"> in the layout, or you will emit two competing graphs.

Multi-site is a first-class case — use localizations, not conditionals

Every SeoSet has a SeoSetLocalization per site. If you are tempted to write @if (Statamic::currentSite() === 'de') in your layout to tweak a title, stop — set the localized value on the SEO set instead. This is the single biggest maintainability win of the package over hand-rolled meta tag partials, and the most common way teams accidentally give it up.

Eloquent driver: write for both

If your project uses statamic/eloquent-driver, the SEO data moves to the database. The repository contracts in Contracts/ are identical, so your application code should not care. But if you are extending the package (adding a new data type or field), you must implement it in both Stache/Repositories/ and Eloquent/. A PR that only implements Stache will break half the install base.

Preview / social image rendering

Social share previews are generated on the fly with spatie/browsershot v5 (headless Chromium). This means:

  • The server needs Node + Puppeteer/Chromium available.
  • Previews are expensive; in local dev and CI they are the first thing to disable if the queue is slow.
  • Do not generate previews on every page save synchronously if you have many editors — queue it.

Gotchas and Tips

All SEO output is computed state — never read or write it from entries

This is stated explicitly in the package's own agent guidelines and it is the rule people break first. There is no $entry->seo_title. There is no $entry->save(['meta_description' => ...]). If you find yourself writing $entry->set('seo_...'), you have left the domain layer and are now coupled to the storage schema. Go through Seo::find(...) and the SeoSet API.

Blink caching will bite you

SEO resolution is cached in Blink under namespaced keys: advanced-seo::{type}::{handle}::{suffix}. Consequences:

  • Changing an entry's content in a seeder or a migration will not invalidate the cache the way a CP save does. Call SeoSet::flushBlink() (or flush the specific prefix) after programmatic writes, or you will ship stale titles.
  • If you cache the rendered {{ seo }} output at the template/full-page level on top of Blink, you now have two caches to invalidate. Don't add a third.
  • In tests, a stale Blink cache between test cases can make you think your code is broken. Flush in setUp.

BrowserSshot / previews fail differently than you expect

If spatie/browsershot cannot launch Chromium, social previews break — but the rest of the SEO output still renders fine. This means previews can be broken in production for weeks without anyone noticing. Add an explicit smoke test or health check for the preview endpoint if social sharing matters to your client.

Use saveQuietly() in setup/migration code

When you are provisioning SEO sets programmatically (seeders, deploy scripts, custom Artisan commands), the normal save path fires Statamic events, which can recursively trigger re-indexing or cache flushes. Use saveQuietly() in setup code — the package's own guidance says to.

Tests: Pest, and protect the Stache

  • The package uses Pest, not PHPUnit. If you are contributing, do not convert tests. Run vendor/bin/pest from the package root.
  • Use the PreventsSavingStacheItemsToDisk trait in any test that creates Statamic content, or your test suite will write YAML files into the project's content/ directory and pollute the repo.
  • Run vendor/bin/pint (Laravel preset) before finalizing any contribution — CI will catch you otherwise.

Version / maturity caveat

This is an 18-star package on a very recent release track with hard floors of PHP 8.4 / Laravel 12 / Statamic 6. That is excellent for greenfield Statamic 6 projects, and a non-starter for anything on Statamic 5 or PHP 8.3. Treat the public API (facade methods, Context signature, cascade data shape) as stabilizing rather than frozen: pin the version in composer.json, and prefer the documented extension points over reaching into Stache/ or Eloquent/ internals, since those are the parts most likely to move between minor releases.

Debugging checklist

When a title or description is not what you expect, resolve in this order:

  1. Is the entry saved after the change? (Blink invalidates on save, not on disk edits.)
  2. Is the value set at site level and being overridden at collection level?
  3. Is the site localization you are viewing the one you edited?
  4. Does Seo::find('collections::X')->config() return what the CP shows? If yes, the problem is downstream in the cascade, not in storage.
  5. Clear Blink (SeoSet::flushBlink()) and re-check before assuming a code bug.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle