# Introduction

Advanced SEO is a comprehensive SEO addon for Statamic that adapts to your project's needs while providing an intuitive interface for content editors.

Advanced SEO provides everything you need to get your SEO right. It's built around a polished editing experience with token-based inputs, live-updating previews, and cascading defaults. It handles everything for great search rankings and social sharing, from complete meta output and structured data to sitemaps, redirects, and 404 error tracking. It's available in two [editions](/getting-started/editions), with Pro adding redirects, 404 error tracking, multi-site support, AI copywriting, granular permissions, GraphQL, and more.

## Features

* Complete meta output with zero configuration
* Intuitive editing experience with live-updating search and social previews
* Site, collection, and taxonomy defaults with cascading inheritance
* Sitemaps, redirects, 404 error tracking, multi-site, AI copywriting, permissions, and more with [Pro](/getting-started/editions)

See [Editions](/getting-started/editions) for the full feature breakdown.

## Requirements

* PHP 8.4+
* Laravel 12+
* Statamic 6+

## License

Advanced SEO is available in two editions. The **Free** edition can be used without a license. The **Pro** edition requires a [license from the Statamic Marketplace](https://statamic.com/addons/aerni/advanced-seo). See [Editions](/getting-started/editions) for details.

## Credits

Developed by [Michael Aerni](https://michaelaerni.ch)


# Editions

Advanced SEO is available in two editions — a free edition for essential on-page SEO and a Pro edition that unlocks sitemaps, redirects, multi-site, AI, and more.

Advanced SEO is available in two editions. The **Free** edition covers everything you need for solid on-page SEO, with dynamic token inputs, reset-to-default, and live-updating search and social previews. The **Pro** edition unlocks advanced features like sitemaps, redirects, 404 error tracking, multi-site support, and AI copywriting.

## Free

Everything you need for solid on-page SEO. Compose titles and descriptions using dynamic tokens pulled from your content fields, preview how they look in search results and social media, and reset any value back to its inherited default.

* [Complete meta output](/reference/meta-tags-and-scripts)
* [Intuitive editing with live previews](/usage/on-page-seo)
* [Global site defaults](/usage/settings-and-defaults#site-defaults)
* [Collection and taxonomy defaults](/usage/settings-and-defaults#collection--taxonomy-defaults)
* Git integration
* [Migration from SEO Pro and Aardvark SEO](/getting-started/installation#migration)

## Pro

Unlock the full potential of Advanced SEO. Includes everything from the Free edition, plus:

* [Sitemaps](/usage/sitemaps)
* [Redirects](/usage/redirects)
* Multi-site support
* [AI copywriting](/usage/ai)
* [Social images generator](/usage/social-images-generator)
* [Granular permissions](/usage/permissions)
* [Custom routes](/extending/custom-routes)
* [Custom tokens](/extending/tokens)
* [GraphQL](/reference/graphql)
* [Eloquent driver](/usage/configuration#eloquent-driver)

## License

The **Free** edition can be used without a license. The **Pro** edition requires a [license from the Statamic Marketplace](https://statamic.com/addons/aerni/advanced-seo).


# Installation

Install Advanced SEO in your Statamic project using Composer.

Install the addon with Composer:

```shell
composer require aerni/advanced-seo
```

Then run the install command to complete the setup:

```shell
php artisan seo:install
```

The command will publish the config, add the required SEO tags to your layout, and ask you about optional features like sitemaps, redirects, social images generation, and AI copywriting. If you're migrating from SEO Pro or Aardvark SEO, the installer will also offer to migrate your existing content.

## Migration

Switching from SEO Pro or Aardvark SEO? Advanced SEO can automatically migrate your existing data. The install command offers this during setup, but you can also run it separately at any time:

```shell
php please seo:migrate
```


# Upgrade 2.0 to 3.0

Upgrade from Advanced SEO 2.0 to 3.0. Most breaking changes are handled by automated upgrade scripts.

Advanced SEO 3.0 introduces a significant internal refactor that improves the architecture, maintainability, and user experience of the addon. Most breaking changes have automated upgrade scripts, so the upgrade should be seamless for most users. First read through this guide to see if there's anything you need to adjust. Most items only apply if you've customized blueprints, published views, or written custom code against the GraphQL or sitemap APIs.

## Upgrade using Composer

In your `composer.json`, change the requirement from:

```diff
- "aerni/advanced-seo": "^2.0"
+ "aerni/advanced-seo": "^3.0"
```

Then run:

```shell
composer update aerni/advanced-seo --with-dependencies
```

Automated upgrade scripts should run right after installation. If they don't trigger, see [Troubleshooting](#troubleshooting).

## Upgrade Summary

The following changes require manual action. All other changes are [handled automatically](#automated-changes).

| Change                                                                | Impact | Action                         |
| --------------------------------------------------------------------- | ------ | ------------------------------ |
| [Screenshot package](#screenshot-package)                             | High   | Require package manually       |
| [Blueprint customization removed](#blueprint-customization-removed)   | High   | Remove custom fields           |
| [GraphQL](#graphql)                                                   | Medium | Update queries                 |
| [Custom sitemap registration](#custom-sitemap-registration)           | Medium | Update code                    |
| [Events](#events)                                                     | Medium | Update event listeners         |
| [Custom views (Twitter)](#custom-views)                               | Low    | Update published views         |
| [Unified social image field](#unified-social-image-field)             | Low    | Update published views         |
| [Generator config changes](#generator-config-changes)                 | Low    | Remove from published config   |
| [Removed Twitter image generation](#removed-twitter-image-generation) | Low    | Remove custom templates        |
| [Social images template changes](#template-changes)                   | Low    | Update custom templates        |
| [Image storage directory](#image-storage-directory)                   | Low    | Delete orphaned images         |
| [Twitter card setting](#twitter-card-setting)                         | Low    | Set card size in site defaults |
| [Sitemap settings](#sitemaps)                                         | Low    | Review sitemap settings        |

## High Impact Changes

### Requirements

* PHP 8.4+
* Laravel 12+
* Statamic 6+

### Screenshot Package

Affects users of the social images generator.

The social images generator now uses [spatie/laravel-screenshot](https://spatie.be/docs/laravel-screenshot/v1/introduction) and is no longer bundled as a dependency. This package supports Browsershot and Cloudflare Browser Rendering as screenshot drivers.

To restore existing functionality, require both the screenshot package and Browsershot:

```shell
composer require spatie/laravel-screenshot spatie/browsershot
```

Alternatively, you can use Cloudflare Browser Rendering instead of Browsershot. Refer to the [installation guide](https://spatie.be/docs/laravel-screenshot/v1/installation-setup) for details. To customize screenshot settings, publish the config:

```shell
php artisan vendor:publish --tag=laravel-screenshot-config
```

### Blueprint Customization Removed

Affects users who customized Advanced SEO fieldsets or used the `advanced_seo` fieldtype in their blueprints.

Support for blueprint customization has been removed. This includes:

* The **Advanced SEO fieldsets** (e.g. `Advanced SEO (Main)`, `Title & Description`, `Open Graph`, etc.) that could be edited to customize which fields appear in the SEO tab
* The **Advanced SEO fieldtype** (`advanced_seo`) that allowed placing individual SEO fields anywhere in a blueprint

Remove any `advanced_seo` fieldtype fields from your blueprints and delete any customized Advanced SEO fieldsets. The SEO tab fields are now managed entirely by the addon and cannot be customized.

## Medium Impact Changes

### GraphQL

Affects users of the GraphQL API. All GraphQL changes require updating your queries.

{% hint style="info" %}
The GraphQL API has been significantly reworked in 3.0. If you'd rather rewrite your queries from scratch, see the [GraphQL reference](/reference/graphql) for the current API.
{% endhint %}

#### Renamed Query and Types

The `seoDefaults` query has been renamed to `seoSet`:

```diff
- seoDefaults {
+ seoSet {
    ...
  }
```

All type names have been updated to use "Set" instead of "Defaults":

| Old Type Name     | New Type Name                   |
| ----------------- | ------------------------------- |
| `siteDefaults`    | `siteSet`                       |
| `contentDefaults` | `collectionSet` / `taxonomySet` |

#### Flattened Site Set Structure

The `siteDefaults` type previously contained nested sub-types. These have been consolidated into a single flat `siteSet` type:

```diff
  {
-   seoDefaults {
-     site {
-       general { site_name, ... }
-       indexing { noindex, ... }
-       socialMedia { og_image, ... }
-       analytics { fathom_id, ... }
-       favicons { favicon_svg, ... }
-     }
-   }
+   seoSet {
+     site { site_name, noindex, og_image, fathom_id, favicon_svg, ... }
+   }
  }
```

<details>

<summary>Removed sub-types</summary>

| Removed Type          | Replacement                          |
| --------------------- | ------------------------------------ |
| `generalDefaults`     | Fields moved directly onto `siteSet` |
| `indexingDefaults`    | Fields moved directly onto `siteSet` |
| `socialMediaDefaults` | Fields moved directly onto `siteSet` |
| `analyticsDefaults`   | Fields moved directly onto `siteSet` |
| `faviconsDefaults`    | Fields moved directly onto `siteSet` |

</details>

#### Removed `seoSet` Fields

The following fields have been removed from the `siteSet` type as they are now configured per collection/taxonomy:

* `excluded_collections`
* `excluded_taxonomies`
* `social_images_generator_collections`

#### Disabled Features

Fields belonging to disabled features are now completely removed from the schema. Previously, disabled feature fields were still present but returned `null`. Now, if a feature is disabled in `config/advanced-seo.php`, its fields will not appear in the schema at all.

#### Removed `baseUrl` Argument

The `baseUrl` argument has been removed from the `seoMeta` query. To rewrite absolute URLs for headless setups, configure your frontend domain in Statamic's sites configuration (`resources/sites.yaml`) instead.

```diff
  {
-   seoMeta(id: "...", baseUrl: "https://frontend.example.com") {
+   seoMeta(id: "...") {
      computed { canonical, hreflang, site_schema, breadcrumbs }
    }
  }
```

#### `computed` Field Changes

| Change  | Field                | Notes                                                      |
| ------- | -------------------- | ---------------------------------------------------------- |
| Added   | `twitter_card`       | Returns the card size (`summary` or `summary_large_image`) |
| Removed | `title`              | Use raw data `title` field                                 |
| Removed | `og_title`           | Use raw data `og_title` field                              |
| Removed | `twitter_title`      | Use raw data `og_title` field                              |
| Removed | `twitter_image`      | Use raw data `og_image` field                              |
| Removed | `og_image`           | Use raw data `og_image` field                              |
| Removed | `generated_og_image` | Use raw data `og_image` field                              |

#### `siteSet` Field Changes

| Change  | Field                                                  | Notes                                                      |
| ------- | ------------------------------------------------------ | ---------------------------------------------------------- |
| Added   | `twitter_card`                                         | Returns the card size (`summary` or `summary_large_image`) |
| Renamed | `title_separator` → `separator`                        |                                                            |
| Renamed | `cloudflare_web_analytics` → `cloudflare_beacon_token` | More descriptive name                                      |
| Renamed | `google_tag_manager` → `gtm_container_id`              | More descriptive name                                      |
| Removed | `nofollow`                                             | Now per-collection/entry only                              |
| Removed | `use_fathom`                                           | Check `fathom_id` presence instead                         |
| Removed | `use_cloudflare_web_analytics`                         | Check `cloudflare_beacon_token` presence instead           |
| Removed | `use_google_tag_manager`                               | Check `gtm_container_id` presence instead                  |
| Removed | `twitter_summary_image`                                | Use `og_image`                                             |
| Removed | `twitter_summary_large_image`                          | Use `og_image`                                             |

#### `seoMeta` Raw Type Field Changes

| Change  | Field                         | Notes                                                                             |
| ------- | ----------------------------- | --------------------------------------------------------------------------------- |
| Removed | `site_name_position`          | Composed into `title`                                                             |
| Removed | `twitter_card`                | Use `seoSet { site { twitter_card } }` or `seoMeta { computed { twitter_card } }` |
| Removed | `twitter_title`               | Use `og_title`                                                                    |
| Removed | `twitter_description`         | Use `og_description`                                                              |
| Removed | `twitter_summary_image`       | Use `og_image`                                                                    |
| Removed | `twitter_summary_large_image` | Use `og_image`                                                                    |
| Removed | `sitemap_priority`            | Google and Bing ignore this value                                                 |
| Removed | `sitemap_change_frequency`    | Google and Bing ignore this value                                                 |

#### `collectionSet` and `taxonomySet` Field Changes

| Change  | Field                         | Notes                                                                             |
| ------- | ----------------------------- | --------------------------------------------------------------------------------- |
| Removed | `site_name_position`          | Composed into `title`                                                             |
| Removed | `twitter_card`                | Use `seoSet { site { twitter_card } }` or `seoMeta { computed { twitter_card } }` |
| Removed | `twitter_title`               | Use `og_title`                                                                    |
| Removed | `twitter_description`         | Use `og_description`                                                              |
| Removed | `twitter_summary_image`       | Use `og_image`                                                                    |
| Removed | `twitter_summary_large_image` | Use `og_image`                                                                    |
| Removed | `sitemap_priority`            | Google and Bing ignore this value                                                 |
| Removed | `sitemap_change_frequency`    | Google and Bing ignore this value                                                 |
| Removed | `canonical_type`              | Per-entry/term only (still on `seoMeta`)                                          |
| Removed | `canonical_entry`             | Per-entry/term only (still on `seoMeta`)                                          |
| Removed | `canonical_custom`            | Per-entry/term only (still on `seoMeta`)                                          |

#### `seoSitemaps` Query

The `seoSitemaps` query has been completely restructured for a simpler, flatter API:

{% tabs %}
{% tab title="Before (Advanced SEO 2.0)" %}

```graphql
{
  seoSitemaps {
    collection(baseUrl: "https://frontend.example.com", site: "default", handle: "pages") {
      loc
      lastmod
      alternates { hreflang, href }
    }
  }
}
```

{% endtab %}

{% tab title="After (Advanced SEO 3.0)" %}

```graphql
{
  seoSitemaps(site: "default", type: "collection", handle: "pages") {
    id
    type
    handle
    lastmod
    urls {
      loc
      lastmod
      alternates { hreflang, href }
    }
  }
}
```

{% endtab %}
{% endtabs %}

Key changes:

* `site` is now a required query argument (was optional on nested fields)
* `type` argument (`collection`, `taxonomy`, `custom`) replaces the nested fields
* `baseUrl` has been removed. Configure your frontend domain in `resources/sites.yaml` instead.
* Returns a list of sitemaps with `id`, `type`, `handle`, `lastmod`, and nested `urls` array

### Custom Sitemap Registration

Affects users who registered custom sitemaps.

The custom sitemaps API has been reworked. Closure-based registration via `Sitemap::register()` is no longer supported. Custom sitemaps now explicitly declare their site using the `$site` property, and the sitemap index automatically includes them. The site defaults to the default Statamic site if not set.

#### Inline Sitemaps

Drop the `Sitemap::register()` closure wrapper and call `register()` directly on the sitemap:

```diff
- Sitemap::register(function () {
-     $signIn = Sitemap::makeUrl('https://example.com/sign-in');
-     $pricing = Sitemap::makeUrl('https://example.com/pricing')
-         ->changefreq('weekly')
-         ->priority('0.8');
-
-     return Sitemap::make('marketing')
-         ->add($signIn)
-         ->add($pricing);
- });
+ Sitemap::make('marketing')
+     ->site('english')
+     ->add('https://example.com/sign-in')
+     ->add('https://example.com/pricing', fn (CustomSitemapUrl $url) => $url
+         ->changefreq('weekly')
+         ->priority('0.8'))
+     ->register();
```

#### Class-Based Sitemaps

Class-based sitemaps now extend `CustomSitemap` instead of `BaseSitemap`. Define the `$handle` and `$site` as properties and use `$this->makeUrl()` instead of the facade:

```diff
- use Aerni\AdvancedSeo\Sitemaps\BaseSitemap;
+ use Aerni\AdvancedSeo\Sitemaps\Custom\CustomSitemap;

- class MarketingSitemap extends BaseSitemap
+ class MarketingSitemap extends CustomSitemap
  {
+     protected string $handle = 'marketing';
+
+     protected string $site = 'english';
+
      public function urls(): Collection
      {
          return collect([
-             Sitemap::makeUrl('https://example.com/sign-in'),
+             $this->makeUrl('https://example.com/sign-in'),
          ]);
      }
  }
```

You can register class-based sitemaps in a service provider or via the config:

```php
// In a service provider's boot method
MarketingSitemap::register();

// Or in config/advanced-seo.php
'sitemap' => [
    'custom' => [
        App\Sitemaps\MarketingSitemap::class,
    ],
],
```

See [Custom Sitemaps](/extending/sitemaps) for the full API reference.

### Events

Affects users listening for `SeoDefaultSetSaved`.

The `SeoDefaultSetSaved` event has been renamed to `SeoSetLocalizationSaved`. The public property has been renamed from `$defaults` to `$localization`.

New events have been added:

* `SeoSetConfigSaved` — Fired when a set's configuration is saved
* `SeoSetConfigDeleted` — Fired when a set's configuration is deleted
* `SeoSetLocalizationDeleted` — Fired when a localization is deleted

## Low Impact Changes

### Custom Views

Affects users who published or customized the `_twitter.antlers.html` or `_analytics.antlers.html` snippets.

Update the following references:

| Old Variable                   | New Variable                  |
| ------------------------------ | ----------------------------- |
| `seo:twitter_title`            | `seo:og_title`                |
| `seo:twitter_description`      | `seo:og_description`          |
| `seo:twitter_image`            | `seo:og_image`                |
| `seo:twitter_image:alt`        | `seo:og_image:alt`            |
| `seo:cloudflare_web_analytics` | `seo:cloudflare_beacon_token` |
| `seo:google_tag_manager`       | `seo:gtm_container_id`        |

### Unified Social Image Field

Affects users who published or customized views referencing `seo:generated_og_image`.

The `seo_generated_og_image` field has been removed. The `seo_og_image` field now handles both generated and user-defined images automatically based on the `seo_generate_social_images` toggle state.

When the toggle is enabled, `seo_og_image` returns the generated image. When disabled, it returns the user-defined image.

Update any views that reference `seo:generated_og_image` to use `seo:og_image`.

### Generator Config Changes

Affects users who published `config/advanced-seo.php` with the `generate_on_save` option.

The `generate_on_save` option has been removed from the `generator` config:

```diff
  'generator' => [
-     'generate_on_save' => true,
  ],
```

Remove this option from your published config file if present.

Social image generation now works as follows:

* **After saving** — Images are generated using Laravel's `defer()`, which runs after the response is sent (sync driver) or dispatches a queued job. Content hashing ensures images are only regenerated when content actually changes.
* **On demand** — If a generated image is missing, it will be regenerated on-the-fly on the next frontend request.

### Removed Twitter Image Generation

Affects users with custom social images generator themes.

The social images generator no longer generates separate Twitter images. Only the Open Graph image is generated. The `twitter_summary` and `twitter_summary_large_image` config presets are still used to resize the shared image for the Twitter meta tags via Glide, but no separate images are generated.

Remove the `twitter_summary.antlers.html` and `twitter_summary_large_image.antlers.html` templates from your custom themes. Only `open_graph.antlers.html` is used.

### Template Changes

Affects users whose social image templates reference the `$group` variable.

The `$group` variable has been removed from the data passed to social images generator templates. If your templates reference this variable, update them.

### Image Storage Directory

Affects users of the social images generator.

Generated social images are now stored in `social_images/collection-{handle}/` and `social_images/taxonomy-{handle}/` subdirectories instead of `social_images/{handle}/`. This adds support for taxonomy terms and avoids collisions between collections and taxonomies with the same handle.

Existing images in the old directory structure will not be migrated automatically. They will be regenerated in the new location on the next save or frontend request. You may delete the orphaned images in the old directories.

### Twitter Card Setting

Affects users who previously set the X (Twitter) card size to `summary` (Small) on any entry, term, or collection/taxonomy default.

The X (Twitter) card size is no longer configurable per entry, term, or collection/taxonomy. It has been moved to the site defaults and applies globally. The default card size is `summary_large_image` (Large). If you previously used `summary` (Small), set your preferred card size in the site defaults after upgrading.

## Automated Changes

Everything in this section is handled by upgrade scripts. No action required. These changes are documented for reference.

### Pro Edition

Existing users are automatically upgraded to the Pro edition by adding `'aerni/advanced-seo' => 'pro'` to `config/statamic/editions.php`. This preserves full feature access since Advanced SEO 2.0 didn't have a free edition.

### Permissions

The permission system has been simplified. Old granular permissions have been replaced with three new permissions:

* `configure seo` — Full access to all SEO settings, defaults, and content editing
* `edit seo defaults` — Edit collection and taxonomy defaults
* `edit seo content` — Access the SEO tab on entries and terms

All existing roles receive the `edit seo content` permission to maintain backward compatibility.

{% hint style="info" %}
Advanced SEO permissions now layer on top of Statamic's native collection and taxonomy permissions:

* To **edit SEO defaults** for a collection or taxonomy, a user needs `edit seo defaults` (or `configure seo`) **and** either Statamic's `configure collections` / `configure taxonomies` or `edit {handle} entries` / `edit {handle} terms` permission.
* To **configure SEO settings** for a collection or taxonomy, a user needs `configure seo` **and** Statamic's `configure collections` / `configure taxonomies` permission.

We recommend reviewing your roles after upgrading to confirm users have the right combination of Advanced SEO and Statamic permissions.
{% endhint %}

### Single-Site Data Structure

Single-site installations now use the same data structure as multi-site, with site-scoped localization files:

```diff
  content/seo/collections/pages.yaml
+ content/seo/collections/{site}/pages.yaml
```

### Site SEO Sets

The five separate site SEO sets (`site::general`, `site::indexing`, `site::social_media`, `site::analytics`, `site::favicons`) have been consolidated into a single `site::defaults` set, with tabs providing the UI grouping. For file-based installations, the file structure has changed accordingly:

```diff
- content/seo/site/{general,indexing,social_media,analytics,favicons}.yaml
- content/seo/site/{site}/{general,indexing,social_media,analytics,favicons}.yaml
+ content/seo/site/defaults.yaml
+ content/seo/site/{site}/defaults.yaml
```

### Disabled Collections and Taxonomies

The `disabled` configuration option has been removed from `config/advanced-seo.php`. Collections and taxonomies are now disabled individually through the Control Panel via the "Configure" button on each set.

```diff
- 'disabled' => [
-     'collections' => [],
-     'taxonomies' => [],
- ],
```

### Localization Origin Field

The `origin` field has been removed from localizations. Origin configuration is now stored centrally in the set config using an `origins` array.

### Eloquent Driver

The `advanced_seo_defaults` table has been replaced by `seo_set_localizations`. A new `seo_set_configs` table stores configuration data (enabled state, origins, etc.). New migrations are published and run automatically, and the old table is dropped after data is migrated.

### SEO Field Values

#### Simplified Field UI

The SEO source fields have been simplified from a three-state toggle (Auto/Default/Custom) to a two-state inheritance model. Fields are now either **inherited** (showing the default value) or **custom** (user-set value). The toggle has been removed. Transitioning between states now happens implicitly through editing and a Reset action.

#### Field Handle Syntax Replaced

The `@field:handle` syntax for referencing other fields has been replaced with standard Antlers `{{ handle }}` syntax. You can now use the full power of Antlers in your SEO fields, including modifiers like `{{ content | truncate(90, '...') }}`. The `@auto` and `@null` sentinel values are also migrated.

#### Site Name Position Composed into Title

The `seo_site_name_position` field has been removed. Its value is composed directly into the `seo_title` field using Antlers syntax, giving you full control over the title format:

| Position   | Resulting `seo_title`                         |
| ---------- | --------------------------------------------- |
| `start`    | `{{ site_name }} {{ separator }} {{ title }}` |
| `end`      | `{{ title }} {{ separator }} {{ site_name }}` |
| `disabled` | `{{ title }}`                                 |

#### Title Separator Renamed

The `title_separator` field in site defaults has been renamed to `separator`.

### X (Twitter) Social Sharing

The X (Twitter) social sharing has been significantly simplified. Instead of maintaining separate Twitter-specific fields, the addon now uses a unified social image shared between Open Graph and Twitter. The card size is now a [site-level default](#twitter-card-setting).

#### Removed Fields

The following fields have been removed from entries, terms, and SEO set localizations:

| Removed Field                     | Replacement                           |
| --------------------------------- | ------------------------------------- |
| `seo_twitter_card`                | [Site default](#twitter-card-setting) |
| `seo_twitter_title`               | Uses `seo_og_title`                   |
| `seo_twitter_description`         | Uses `seo_og_description`             |
| `seo_twitter_summary_image`       | Uses `seo_og_image`                   |
| `seo_twitter_summary_large_image` | Uses `seo_og_image`                   |

The following fields have been removed from the site-wide social media defaults:

| Removed Field                 | Replacement     |
| ----------------------------- | --------------- |
| `twitter_summary_image`       | Uses `og_image` |
| `twitter_summary_large_image` | Uses `og_image` |

### Nofollow Removed from Site Defaults

The site-wide **nofollow** toggle has been removed from the site defaults. It has no practical use case (if you need to prevent crawling entirely, the `crawling.environments` configuration already forces both noindex and nofollow when the environment is not listed). Nofollow is now managed per collection/taxonomy in the content defaults and per entry/term.

### Social Images Generator

#### Per-Collection Settings

The `social_images_generator_collections` field has been removed from the site defaults. The social images generator is now enabled per-collection using the `social_images_generator` toggle in the collection config.

#### Removed Social Image Preview Targets

The social images generator no longer registers live preview targets for entries. Social image previews are now shown directly in the publish form via a new inline preview fieldtype with real-time theme switching.

### Canonical URL

#### Removed from Collection/Taxonomy Defaults

The canonical URL fields (`seo_canonical_type`, `seo_canonical_entry`, `seo_canonical_custom`) have been removed from collection and taxonomy defaults. Canonical URLs are now configured per-entry/term only, since setting a default canonical for an entire collection is not a meaningful SEO configuration.

Entries and terms that inherited the default `current` canonical type are unaffected — `current` is now the hardcoded default on the entry/term level.

#### Renamed Canonical Type and New Term Support

The `other` canonical type has been renamed to `entry`. The canonical type field now uses a button group with three options: **Current**, **Entry**, and **URL**. If you reference `seo_canonical_type` values in code or templates, update `other` to `entry`.

### Sitemaps

#### Per-Collection/Taxonomy Settings

The `excluded_collections` and `excluded_taxonomies` fields have been removed from the site defaults. The sitemap is now enabled per-collection/taxonomy using the `sitemap` toggle in the collection/taxonomy config.

{% hint style="warning" %}
If a collection or taxonomy was excluded from the sitemap in **any** site, the migration conservatively disables the sitemap for that entire collection/taxonomy. After upgrading, review your sitemap settings in the Control Panel and re-enable any collections or taxonomies that should be included.
{% endhint %}

#### Removed Priority and Change Frequency Fields

The **Priority** and **Change Frequency** sitemap fields have been removed from entries, terms, and collection/taxonomy defaults. Google and Bing ignore these values. Auto-generated sitemaps no longer output `<priority>` or `<changefreq>` XML elements, though the [custom sitemaps API](/extending/sitemaps) still supports them.

#### Domain Scoping

Each domain now gets its own sitemap index, and its sitemaps only contain URLs from sites on that domain. Previously, a single sitemap index included URLs from all sites regardless of domain.

| Setup                                             | Sitemap indexes                                 |
| ------------------------------------------------- | ----------------------------------------------- |
| `example.com`, `example.com/de`, `example.com/fr` | One index on `example.com` (unchanged)          |
| `example.com`, `example.com/de`, `example.fr`     | One index on `example.com`, one on `example.fr` |
| `example.com`, `example.de`, `example.fr`         | One index per domain                            |

{% hint style="info" %}
Hreflang tags are unaffected and continue to reference all localized versions across domains.
{% endhint %}

## Troubleshooting

If the automated upgrade script fails or doesn't run, you can trigger it manually using Statamic's built-in command. Pass any version below `3.0.0` as the version you are upgrading from:

```shell
php please updates:run 2.0.0 --package=aerni/advanced-seo
```


# Configuration

Configure storage drivers, crawling, redirects, analytics, the view composer, and other options in config/advanced-seo.php.

Advanced SEO is configured through `config/advanced-seo.php`. The config file is automatically published during [installation](/getting-started/installation). To publish it manually:

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

{% hint style="info" %}
When a feature is disabled in the config, all its associated fields are removed from the blueprints.
{% endhint %}

## Storage Driver

Advanced SEO supports two storage drivers: `file` and `eloquent`. The File driver stores data as flat-files alongside your content, while the Eloquent driver stores data in a database using the [Statamic Eloquent Driver](https://github.com/statamic/eloquent-driver).

### File Driver

The File driver is the default. Data is saved in `content/seo`, but you can customize the directory:

```php
'driver' => 'file',
'directory' => base_path('content/seo'),
```

To switch back to flat-files from Eloquent, run the following command. It will update the config and give you the option to export existing data from the database to flat-files:

```shell
php please seo:switch-to-file
```

### Eloquent Driver

{% hint style="info" %}
The Eloquent driver requires the [Pro edition](/getting-started/editions).
{% endhint %}

To use the Eloquent driver, first install the [Statamic Eloquent Driver](https://github.com/statamic/eloquent-driver/tree/master):

```shell
composer require statamic/eloquent-driver
```

Then run the following command. It will set the driver in your config to `eloquent`, publish and run the database migrations, and give you the option to import existing data from flat-files into the database:

```shell
php please seo:switch-to-eloquent
```

{% hint style="info" %}
Neither command will delete any existing data from the previous driver. You may clean up old flat-files or database tables yourself once you're satisfied with the migration.
{% endhint %}

## GraphQL

Enable the GraphQL API. See [GraphQL](/reference/graphql) for available queries and types.

```php
'graphql' => false,
```

{% hint style="info" %}
Statamic's GraphQL API must also be enabled. Refer to the [Statamic documentation](https://statamic.dev/graphql) for setup.
{% endhint %}

## View Composer

Advanced SEO uses a view composer to inject the `seo` variable into your Antlers and Blade views. This is what makes values like `{{ seo:title }}` available in your templates. By default, the view composer is applied to all views (`'*'`). If you want to limit which views receive SEO data:

```php
'view_composer' => [
    'layouts.app',
    'pages.*',
],
```

## Social Images

### Asset Container

Configure the asset container for social images. Generated images are saved in a `social_images` directory within the configured container:

```php
'social_images' => [
    'container' => 'assets',
],
```

### Presets

Customize the dimensions of social images:

```php
'social_images' => [
    'presets' => [
        'open_graph' => ['width' => 1200, 'height' => 630],
        'twitter_summary' => ['width' => 240, 'height' => 240],
        'twitter_summary_large_image' => ['width' => 1200, 'height' => 630],
    ],
],
```

The `open_graph` preset defines the generated image size. The `twitter_summary` and `twitter_summary_large_image` presets define the dimensions used to resize the shared image for the Twitter meta tags via Glide.

### Generator

The generator automatically creates social images from customizable Antlers templates. See [Social Images Generator](/usage/social-images-generator) for full setup instructions.

```php
'social_images' => [
    'generator' => [
        'enabled' => false,
        'queue' => 'default',
    ],
],
```

## Favicons

The favicons feature renders favicon meta tags from the SVG uploaded in [site defaults](/usage/settings-and-defaults#site-defaults). You can disable it if you manage favicons yourself:

```php
'favicons' => [
    'enabled' => true,
    'container' => 'assets',
],
```

## Crawling

The crawling config controls which environments allow search engine crawling. By default, crawling is only enabled for the `production` environment:

```php
'crawling' => [
    'environments' => ['production'],
],
```

In all other environments:

* The robots meta tag is set to `noindex, nofollow`
* Crawling-related meta tags like `canonical` and `alternate` are removed
* Sitemap routes are disabled

## Sitemaps

Domain-scoped XML sitemaps are generated automatically. See [Sitemaps](/usage/sitemaps) for details.

```php
'sitemap' => [
    'enabled' => true,
    'path' => storage_path('statamic/sitemaps'),
    'queue' => 'default',
    'custom' => [
        //
    ],
],
```

## Redirects

{% hint style="info" %}
Redirects require the [Pro edition](/getting-started/editions). See [Redirects](/usage/redirects) for automatic generation, errors, and import/export.
{% endhint %}

Redirects ship disabled. Enabling the feature activates redirect handling and automatic redirect generation:

```php
'redirects' => [
    'enabled' => true,
],
```

Disabling the feature removes its Control Panel navigation and stops redirect handling, hit tracking, error tracking, and automatic generation.

### Storage

With the File driver, redirects are stored in `content/redirects`. Hit and error records live in storage by default:

```php
'redirects' => [
    'directory' => base_path('content/redirects'),
    'hits' => [
        'directory' => storage_path('statamic/advanced-seo/redirect-hits'),
    ],
    'errors' => [
        'directory' => storage_path('statamic/advanced-seo/redirect-errors'),
    ],
],
```

These directory options are ignored when using the Eloquent driver. Redirects, hits, and errors are stored in their corresponding database tables instead.

### Hit Tracking

Hit tracking records how often each redirect is used and when it was last hit. It includes `410 Gone` responses.

```php
'redirects' => [
    'hits' => [
        'enabled' => true,
    ],
],
```

Set `enabled` to `false` to stop recording hits. Existing statistics are preserved.

### Error Tracking

Error tracking records frontend paths that return an unhandled 404:

```php
'redirects' => [
    'errors' => [
        'enabled' => true,
        'purge_after_days' => 30,
        'max_records' => 1000,
        'ignore' => [
            '#\.php$#',
            '#^/wp-admin#',
            '#^/\.(env|git)#',
        ],
    ],
],
```

* `enabled` — Show the error inbox and record unhandled 404s.
* `purge_after_days` — Delete errors not seen within this many days. Set to `false` to disable age-based pruning.
* `max_records` — Keep the most-hit errors up to this limit. Set to `false` for no limit.
* `ignore` — Exact paths, wildcards, or regex patterns that should never be recorded.

{% hint style="warning" %}
An unlimited error inbox is only recommended with the Eloquent driver. The File driver creates one YAML file for every recorded path.
{% endhint %}

The daily `seo:prune-redirect-errors` command applies retention, deletes handled errors, and enforces the record cap. The cap is best-effort during concurrent requests and may be briefly exceeded before the next write or prune.

### Queue

Redirect hits and errors are recorded through queued jobs. Choose the queue name:

```php
'redirects' => [
    'queue' => 'default',
],
```

When using an asynchronous queue connection, make sure a queue worker processes this queue.

## Analytics

Analytics scripts only render in the configured environments. Defaults to `production`:

```php
'analytics' => [
    'environments' => ['production'],
],
```

You can also disable individual trackers to remove their section from the [site defaults](/usage/settings-and-defaults#site-defaults):

```php
'analytics' => [
    'fathom' => true,
    'cloudflare_analytics' => true,
    'google_tag_manager' => true,
],
```

## Site Verification

Site verification adds Google and Bing verification code fields to the [site defaults](/usage/settings-and-defaults#site-defaults). Disable it to remove those fields:

```php
'site_verification' => true,
```

## Tokens

Register custom token normalizers and value tokens. See [Tokens](/extending/tokens) for details.

```php
'tokens' => [
    //
],
```

## AI

See [AI Copywriting](/usage/ai) for setup and configuration.

```php
'ai' => [
    'enabled' => false,
    'provider' => null,
    'model' => null,
],
```


# Settings & Defaults

Configure site-wide SEO settings, collection and taxonomy defaults, and multi-site data inheritance.

Advanced SEO adds an **SEO** item to the tools section in the Control Panel sidebar. This is where you manage all your SEO settings and defaults.

There are two types of defaults:

1. **Site defaults** — Global settings that apply across your entire site
2. **Collection & Taxonomy defaults** — Default SEO values for each collection and taxonomy

## Cascade

SEO values are resolved through a cascade. Each level serves a different purpose:

* **Site defaults** — Global values like the site name, separator, default OG image, indexing rules, and analytics. These apply across the entire site.
* **Collection & Taxonomy defaults** — Field-level values like title templates, descriptions, and sitemap settings. These are inherited by entries and terms.
* **Entries & Terms** — Per-page overrides set by content editors.

Where fields exist at multiple levels (such as the OG image), values fall back upward: entries and terms fall back to collection and taxonomy defaults, which fall back to site defaults. Site-level noindex/nofollow is the exception: when enabled, it overrides everything below it.

When an editor sets a value on an entry or term, it overrides the inherited default. To revert to the inherited value, use the **Reset to default** button below the field. This removes the override and lets the value cascade down from the defaults again.

## Site Defaults

Site defaults are your global SEO settings. They provide site-wide values like the site name, separator, default social image, indexing rules, and analytics trackers.

The settings are organized into tabs:

* **Search Appearance** — Site name, title separator, structured data (JSON-LD), and favicons
* **Social Appearance** — Default OG image and X (Twitter) handle. The OG image cascades down to collection, taxonomy, entry, and term levels as the fallback image.
* **Indexing** — Global noindex/nofollow and site verification codes. When enabled, these override all collection, taxonomy, entry, and term values.
* **AI** — Global [instructions](/usage/ai#instructions) for AI-generated titles and descriptions (Pro)
* **Analytics** — Fathom, Cloudflare Web Analytics, and Google Tag Manager

{% hint style="info" %}
Analytics scripts only render in the configured [crawling environments](/usage/configuration#analytics).
{% endhint %}

## Collection & Taxonomy Defaults

Each collection and taxonomy has its own set of default values. These are the values that entries and terms inherit when they don't define their own. The available fields mirror the [on-page SEO fields](/usage/on-page-seo) on entries and terms.

For example, you might set a default SEO title template of `{{ title }} {{ separator }} {{ site_name }}` on the Pages collection. Every page will use that template unless an editor overrides it on a specific entry.

### Configuration

Each collection and taxonomy also has a configuration that controls how Advanced SEO behaves for that content type. Access it by clicking the **Configure** button on any collection or taxonomy in the SEO section.

* **Enabled** — Toggle SEO processing on or off. When disabled, no SEO data is computed for its entries/terms and the SEO tab is hidden.
* **Editable** — Controls whether non-super-users see the SEO tab on entries/terms. Useful when you want SEO to be fully driven by defaults or computed values without giving editors control.
* **Origin** — Set the origin site for multi-site inheritance. See [Origins](#origins) below.
* **Sitemap** — Include this collection/taxonomy in the XML sitemap.
* **Redirects** — Automatically create redirects when entry or term URLs change. See [Redirects](/usage/redirects#automatic-redirects).
* **Twitter Card** — Choose between `summary` and `summary_large_image` for entries/terms.
* **Social Images Generator** — Enable automatic social image generation and restrict which themes are available.
* **AI** — Toggle AI copywriting for this collection or taxonomy and provide scoped [instructions](/usage/ai#instructions) (Pro).

## Origins

{% hint style="info" %}
Multi-site support requires the [Pro edition](/getting-started/editions).
{% endhint %}

In a multi-site setup, origins control how SEO defaults are inherited across sites. Each site can inherit its defaults from another site, creating an inheritance chain.

For example, if your German site inherits from your English site, any SEO defaults not explicitly set for German will fall back to the English values. This means you only need to maintain one complete set of defaults and can selectively override values for other sites.


# Permissions

Control which users can view and edit SEO settings, defaults, content, and redirects.

{% hint style="info" %}
Granular permissions require the [Pro edition](/getting-started/editions).
{% endhint %}

Advanced SEO adds four permissions under the **Advanced SEO** group:

| Permission          | Description                                                                                                |
| ------------------- | ---------------------------------------------------------------------------------------------------------- |
| `configure seo`     | Full access to all SEO settings, defaults, configuration, redirects, and the SEO tab on entries and terms. |
| `edit seo defaults` | Edit collection and taxonomy defaults, and access the SEO tab on entries and terms.                        |
| `edit seo content`  | Access the SEO tab on entries and terms.                                                                   |
| `manage redirects`  | View and manage redirects and redirect errors for sites the user can access.                               |

Super users always have full access regardless of permissions.

## How Permissions Work

### Site Defaults

Only users with the `configure seo` permission can edit site defaults.

### Collection & Taxonomy Defaults

Users need the `configure seo` or `edit seo defaults` permission **and** Statamic's native permission for the corresponding content type. For example, to edit SEO defaults for the Pages collection, a user needs `edit seo defaults` plus either Statamic's `configure collections` or `edit pages entries` permission. Accessing the configuration requires `configure seo` and the native `configure collections` or `configure taxonomies` permission.

### SEO Tab on Entries & Terms

The `configure seo`, `edit seo defaults`, and `edit seo content` permissions grant access to the SEO tab. Additionally, the collection or taxonomy must have its [Editable](/usage/settings-and-defaults#configuration) toggle enabled.

### Redirects

Users need the `configure seo` or `manage redirects` permission to access redirects and redirect errors. They must also have access to the relevant Statamic site. In multi-site setups, listings, imports, exports, and bulk actions are limited to the sites each user can access.


# On-Page SEO

Edit SEO fields on individual entries and terms with dynamic tokens, inline previews, and AI copywriting.

## SEO Tab

Advanced SEO adds an **SEO** tab to the blueprints of your collections and taxonomies. This is where you can edit all SEO-related fields for individual entries and terms.

The tab is organized into collapsible sections:

* **Search Appearance** — Meta title, description, and a live search preview.
* **Social Appearance** — OG title, description, image, social images generator settings, and a live social preview.
* **Indexing** — Noindex and nofollow toggles.
* **Canonical URL** — Point to the current page, another entry, or a custom URL. Hidden when noindex is enabled.
* **Sitemap** — Sitemap inclusion toggle.
* **Structured Data** — Custom JSON-LD.

### Visibility

There are two ways to control who sees the SEO tab:

* **Editable toggle** — Hide the SEO tab entirely for a collection or taxonomy via its [configuration](/usage/settings-and-defaults#configuration). Enabled by default. Useful to disable for content types where SEO is fully driven by defaults, like products or automated pages.
* **Permissions** — Control which users can view and edit SEO data via [permissions](/usage/permissions). This lets you restrict access per user role while keeping the tab available for authorized users.

## Field Values & Inheritance

Every SEO field on an entry or term inherits its value from the [defaults](/usage/settings-and-defaults) you've configured. When you edit a field, you override the inherited default. Use the **Reset to default** button below the field to revert to the inherited value. See [Cascade](/usage/settings-and-defaults#cascade) for how values are resolved.

## Token Input

Titles and descriptions use a token-based input field. Tokens are dynamic placeholders that resolve to field values at render time.

### Adding Tokens

Type `/` or click the `+` button in any title or description field to open the token autocomplete, then select a field to insert it as a token. Since tokens use Antlers syntax and support modifiers, you can also type Antlers directly into the field.

```
{{ title }}
{{ title | upper }}
{{ content | truncate(90, '...') }}
```

Tokens render in one of three visual states so you can tell at a glance what kind of value is attached:

* **Blue chip** — a known field token from the entry's blueprint or a registered [custom value token](/extending/tokens#value-tokens). Shows the field's display label.
* **Gray chip** — an Antlers variable the system doesn't recognize as a registered token. This could be an unregistered custom Antlers tag (`{{ company_name }}`), a nested field reference (`{{ foo:bar }}`), or a typo. The variable still resolves correctly on the frontend if it's valid Antlers.
* **Gray chip with a `{}` marker** — a variable with modifiers attached (`{{ title | upper }}`). The `{}` marker signals there's more going on than the name suggests. Double-click to see the full source.

### Editing Tokens

Double-click an existing token (or press Enter when focused) to edit it. This is useful for adding or adjusting Antlers modifiers, or changing the referenced field.

{% hint style="info" %}
Complex Antlers like tag pairs and conditionals is technically supported (it renders correctly on the frontend via `Antlers::parse()`), but isn't supported by the editor UI. Such expressions appear as multiple disjointed chips and plain text rather than a single unit, and editing them inline is unreliable. If you need dynamic values from a tag, register a [custom value token](/extending/tokens#value-tokens) instead. It'll appear as a proper blue chip in the autocomplete.
{% endhint %}

The token input shows a character counter with recommended limits. The counter only works with plain text and simple tokens. Custom Antlers with modifiers can't be counted as the final value isn't known at edit time:

| Field            | Character Limit |
| ---------------- | --------------- |
| Meta Title       | 60              |
| Meta Description | 160             |
| OG Title         | 70              |
| OG Description   | 200             |

Available tokens are derived from the entry or term's blueprint fields. You can also register [custom tokens](/extending/tokens) for additional fieldtypes or custom values.

{% hint style="info" %}
In the content defaults of a collection or taxonomy, the available field tokens are limited to fields that exist in **all** of its blueprints. If a collection has two blueprints and a field only exists in one of them, that field won't be available as a token in the defaults. This ensures the token resolves consistently for every entry or term, regardless of which blueprint it uses.
{% endhint %}

## Antlers in SEO Fields

Most SEO fields are designed for referencing other fields on your entry, e.g. `{{ title }}`, `{{ intro }}`, or chaining common modifiers like `{{ title | upper }}` or `{{ intro | truncate:160 }}`. The JSON-LD field goes further and often uses Antlers tags (`{{ collection }}`, `{{ nav }}`, `{{ glide }}`, loops, etc.) to build custom schemas.

Since Statamic 6.4.0, field content is parsed in Antlers' [user-content mode](https://github.com/statamic/cms/pull/14092), which restricts which tags and modifiers are available:

* **Variables always resolve** — `{{ any_field_handle }}` works everywhere.
* **Common modifiers work out of the box** — Statamic ships a generous default allowlist (`upper`, `lower`, `truncate`, `limit`, `markdown`, `format`, and many more). Your app's own custom modifiers are auto-allowed too.
* **Core tags are mostly restricted by default** — only a handful (`trans`, `widont`, `link`, and a few others) are allowed. Your app's own custom tags are auto-allowed.

If a tag or modifier you use in an SEO field renders nothing or logs a `Runtime Access Violation`, opt into it in `config/statamic/antlers.php`:

```php
'allowedContentTags' => ['@default', 'collection:*', 'glide:*'],
'allowedContentModifiers' => ['@default', 'raw', 'some_modifier'],
```

The `@default` token preserves Statamic's built-in defaults. Tag patterns use `name:*`; modifiers are listed by literal name.

## Search & Social Previews

The SEO tab includes inline previews that update in real time as you edit:

* **Search Preview** — Shows how the entry or term might appear in Google search results, including the title, description, URL, and breadcrumbs.
* **Social Preview** — Shows how the entry or term will appear when shared on Facebook and X (Twitter). The X (Twitter) preview adjusts based on the card setting configured on the collection or taxonomy.

{% hint style="info" %}
Previews are approximate. The actual appearance may vary depending on the platform, device, and other factors.
{% endhint %}

## AI Copywriting

Meta titles, meta descriptions, OG titles, and OG descriptions can be generated by AI directly from the SEO tab. See [AI Copywriting](/usage/ai) for setup, per-collection control, and instructions.


# AI Copywriting

Generate SEO titles and descriptions with AI, with site-wide and per-collection instructions.

{% hint style="info" %}
AI copywriting requires the [Pro edition](/getting-started/editions).
{% endhint %}

Advanced SEO can generate meta titles, meta descriptions, OG titles, and OG descriptions using AI. Generation is powered by the [Laravel AI SDK](https://github.com/laravel/ai), so any provider it supports (OpenAI, Anthropic, Google, etc.) works.

## Setup

Run the install command and select **AI Copywriting**:

```shell
php please seo:install
```

This installs the Laravel AI SDK and enables AI in `config/advanced-seo.php`. After installation, configure your provider in `config/ai.php`.

If you don't want Advanced SEO to use the default provider from `config/ai.php`, set a specific provider and model in `config/advanced-seo.php`:

```php
'ai' => [
    'enabled' => true,
    'provider' => 'anthropic',
    'model' => 'claude-haiku-4-5-20251001',
],
```

## Enabling AI per Collection or Taxonomy

Once AI is enabled in the config, you can control it on a per-collection and per-taxonomy basis. Click the **Configure** button on any collection or taxonomy in the SEO section and toggle **Enable AI** on or off.

When the toggle is off, the generate action is hidden from the entries and terms of that collection or taxonomy. This is useful when AI generation only makes sense for some content types (like blog posts or products) but not others (like legal pages).

## Usage

When AI is enabled, a generate action appears in the meta title, meta description, OG title, and OG description fields on entries and terms. Click it to generate contextual text based on the entry or term's content. The site's language is respected for multilingual generation.

{% hint style="info" %}
The entry or term needs at least 50 characters of content for AI generation to work.
{% endhint %}

## Instructions

You can guide how the AI writes by providing custom instructions. Instructions are configured in two places, and both are combined when generating content:

* **Site defaults** — Global instructions that apply to all AI generation. Set these in the **AI** section of your [site defaults](/usage/settings-and-defaults#site-defaults). Use this for general tone, voice, and vocabulary rules.
* **Collection & Taxonomy configuration** — Instructions specific to one collection or taxonomy. Set these in the **Configure** screen alongside the AI toggle. Use this for content-type-specific context.

Both levels are combined when generating content, so the AI sees your global tone rules together with the scoped context for that collection or taxonomy.


# Sitemaps

Automatically generated XML sitemaps for your collections and taxonomies.

{% hint style="info" %}
Sitemaps require the [Pro edition](/getting-started/editions).
{% endhint %}

Advanced SEO generates sitemaps for all of your collections and taxonomies. The sitemaps are organized in a sitemap index, accessible at `/sitemap.xml`.

## Generating Sitemaps

Sitemaps are generated on demand whenever a sitemap is visited on the frontend. For content-heavy sites, you can pre-generate them with the following command:

```shell
php please seo:generate-sitemaps
```

Add the `--queue` flag to generate the sitemaps in the background:

```shell
php please seo:generate-sitemaps --queue
```

{% hint style="info" %}
Make sure that your current environment is [enabled for crawling](/usage/configuration#crawling). Otherwise, you won't be able to generate the sitemaps.
{% endhint %}

The recommended approach is to [schedule the command](https://laravel.com/docs/master/scheduling#scheduling-artisan-commands):

```php
Schedule::command('seo:generate-sitemaps --queue')->hourly();
```

You may change the storage path for generated sitemaps in the config:

```php
'sitemap' => [
    'path' => storage_path('statamic/sitemaps'),
],
```

## Disable Sitemaps

You may globally disable the sitemap feature in the config:

```php
'sitemap' => [
    'enabled' => false,
],
```

This removes all sitemap-related settings from the Control Panel and disables the sitemap frontend routes.

## Disabling Collections & Taxonomies

You can disable sitemaps for individual collections and taxonomies using the **Sitemap** toggle in the [collection/taxonomy configuration](/usage/settings-and-defaults#collection--taxonomy-configuration).

Sitemaps are also excluded when **Noindex** has been enabled in the collection/taxonomy defaults.

## Excluding Individual Entries and Terms

An entry or term is excluded from the sitemap when any of the following conditions are met:

* **Sitemap toggle** is disabled on the entry or term.
* **Noindex** is enabled on the entry or term.
* **Canonical URL** points to a different entry or a custom URL.

## Domain Scoping

In multi-site setups with multiple domains, each domain gets its own sitemap index. Sitemaps only contain URLs from sites on that domain.

| Setup                                             | Sitemap Indexes                                 |
| ------------------------------------------------- | ----------------------------------------------- |
| `example.com`, `example.com/de`, `example.com/fr` | One index on `example.com`                      |
| `example.com`, `example.com/de`, `example.fr`     | One index on `example.com`, one on `example.fr` |
| `example.com`, `example.de`, `example.fr`         | One index per domain                            |

{% hint style="info" %}
Hreflang tags are unaffected and continue to reference all localized versions across domains.
{% endhint %}

## Custom Sitemaps

Advanced SEO automatically generates sitemaps for collections and taxonomies. If you need to include additional routes (like marketing pages or custom Laravel routes), you can register custom sitemaps using a class-based or inline approach. See [Custom Sitemaps](/extending/sitemaps) for details.


# Redirects

Create and manage manual and automatic redirects, track broken URLs, and import or export redirects.

{% hint style="info" %}
Redirects require the [Pro edition](/getting-started/editions).
{% endhint %}

Advanced SEO sends visitors from old or broken URLs to the right place. If a URL already belongs to published content or another working page, that page is shown instead.

Each redirect belongs to a specific site. In a multi-site setup, the same path can point to a different destination on each site.

## Setup

### Install Command

Run the install command and select **Redirects**:

```shell
php please seo:install
```

The command enables redirects in `config/advanced-seo.php` and installs `spatie/simple-excel` for importing and exporting redirects.

### Manual Setup

Enable redirects directly in your config:

```php
// config/advanced-seo.php
'redirects' => [
    'enabled' => true,
],
```

Redirect handling works without an additional dependency. To also enable importing and exporting, install `spatie/simple-excel`:

```shell
composer require spatie/simple-excel
```

See [Configuration](/usage/configuration#redirects) for all available options.

## Automatic Redirects

Advanced SEO can automatically create permanent redirects when the URL of a published entry or taxonomy term changes. Automatic redirects are enabled by default for every collection and taxonomy after the global redirects feature has been enabled.

To change this per content type, open **SEO** in the Control Panel, choose a collection or taxonomy, click **Configure**, and toggle **Enable Redirects**.

Only published content generates automatic redirects. Changing draft, scheduled, or expired entries does not create one.

### How Redirects Are Generated

When a published entry's slug changes, Advanced SEO creates a `301` redirect from its previous path to the entry. Dated collections are also handled when changing the date changes the entry's URL.

The destination is stored as an entry reference rather than a fixed URL. If the entry moves again, every automatic redirect pointing to it continues to resolve to its current URL.

When a taxonomy term's base slug changes, Advanced SEO creates a redirect for every affected site. Older automatic term redirects are repointed directly to the newest path to avoid redirect chains.

Automatic redirects are also maintained when content changes:

* Deleting an entry or term removes automatic redirects that point to it.
* When an entry or term moves back to a previous URL, Advanced SEO removes the conflicting automatic redirect and creates a new redirect from the URL it just left.
* Existing redirects are never overwritten when automatic generation encounters the same source.

### URL Conflicts and Reused Slugs

Live content wins over redirects. Redirect matching only runs after Statamic determines that the request would otherwise return a 404.

For example, renaming `/about` to `/company` creates an automatic redirect from `/about` to the entry. If another published entry later claims `/about`, that entry is served and the automatic redirect is removed.

Manual redirects are treated as deliberate editor decisions and are not deleted when an entry or term claims the same path. They remain stored but do not override live content. If that path returns a 404 again later, the manual redirect becomes active again.

Advanced SEO does not create an automatic redirect when another redirect already exists with the same source and site.

### Limitations

Automatic generation can reliably detect entry slug changes, URL-changing date changes, and base taxonomy term slug changes. It cannot recover the previous URL for every structural or routing change.

Create redirects manually or use the redirect error inbox when URLs change because of:

* Descendant URLs changing because a parent entry was renamed.
* Moving an entry within a structure or collection tree.
* Changing a collection route or mount.
* Changing a non-slug field used as a route token.
* Changing the overridden slug of an individual term localization.

## Creating Redirects

Open **SEO → Redirects** in the Control Panel and select **Create Redirect**.

Each redirect contains:

* **Source** — The site-relative path or pattern to match. On a multi-site installation, a site selector appears beside this field when you have access to more than one site. Use it to choose which site the redirect belongs to.
* **Destination** — A site-relative path, full URL, or published entry.
* **Response Code** — `301`, `302`, or `410`.
* **Preserve Query String** — Whether to append the request's query string to the destination.
* **Description** — An optional internal note.
* **Enabled** — Keep a redirect without making it active.

### Source Patterns

Sources support exact paths, wildcards, and regular expressions:

| Type     | Source                | Destination    | Example Result                    |
| -------- | --------------------- | -------------- | --------------------------------- |
| Exact    | `/old-page`           | `/new-page`    | `/old-page` → `/new-page`         |
| Wildcard | `/blog/*`             | `/articles/$1` | `/blog/hello` → `/articles/hello` |
| Regex    | `#^/products/(\d+)$#` | `/catalog/$1`  | `/products/42` → `/catalog/42`    |

Each `*` wildcard captures one path segment and does not cross a slash. Multiple wildcards are available as `$1`, `$2`, and so on in the destination. Regex capture groups use the same placeholders.

Exact paths and wildcards are case-insensitive. Regex patterns are case-sensitive unless you add a modifier such as `i`. Trailing slashes are normalized, so `/about` and `/about/` match the same exact redirect.

When multiple redirects match, Advanced SEO prefers:

1. An exact path.
2. The most specific wildcard.
3. A regular expression.

{% hint style="warning" %}
Keep regular expressions focused and inexpensive. They are evaluated against unmatched request paths.
{% endhint %}

### Destinations

A destination can be:

* A path such as `/about`, resolved relative to the source site.
* A full URL such as `https://example.com/about`.
* A published entry selected from Statamic's link field.

Entry destinations follow the entry's current URL when its slug, date, route, or site URL changes.

### Response Codes

| Code            | Use                                                                                   |
| --------------- | ------------------------------------------------------------------------------------- |
| `301 Permanent` | The old URL has permanently moved. This is the default.                               |
| `302 Temporary` | The move is temporary and the source URL may return later.                            |
| `410 Gone`      | The resource was deliberately removed and has no replacement. No destination is used. |

### Query Strings

**Preserve Query String** is enabled by default. A request to `/old?utm_source=newsletter` redirects to `/new?utm_source=newsletter`.

If the destination already contains a query string, the request parameters are appended. Query strings are inserted before URL fragments.

## Managing Redirects

The redirect listing shows each redirect's source, destination, response code, status, origin, hit count, and last hit. Origins indicate whether a redirect was created manually, automatically, through an import, or from a redirect error.

Available actions let you:

* Enable or disable redirects.
* Test enabled exact and wildcard redirects.
* Reset hit statistics.
* Delete redirects individually or in bulk.
* Search and filter by site, status, origin, and response code.

Hit recording runs through the queue configured in `redirects.queue`. Disable it with `redirects.hits.enabled` if you don't need usage statistics.

## Redirect Errors

The **SEO → Errors** page is an inbox of frontend paths that returned an unhandled 404. Advanced SEO groups repeated requests by path and site, and records the hit count plus the first and last time each error was seen.

Control Panel routes, API routes, requests other than `GET` and `HEAD`, and paths matched by `redirects.errors.ignore` are not recorded.

From the error listing, you can:

* Create a redirect with the source and site already filled in.
* Open a matching disabled redirect and enable or edit it.
* Delete selected errors.
* Clear all errors for the sites you can access.

Saving an enabled redirect automatically removes every recorded error it handles, including errors covered by a wildcard or regex source.

### Cleanup and Retention

Advanced SEO schedules `seo:prune-redirect-errors` to run daily when redirects and error tracking are enabled. Make sure [Laravel's scheduler](https://laravel.com/docs/12.x/scheduling#running-the-scheduler) is running in production.

The command:

* Deletes errors that have not been seen within `redirects.errors.purge_after_days`.
* Enforces `redirects.errors.max_records`, removing the least-hit and oldest records first.

The defaults retain errors for 30 days and keep up to 1,000 records. See [Redirect Configuration](/usage/configuration#redirects) to change these limits or ignore noisy paths.

## Import and Export

Import and export are available when `spatie/simple-excel` is installed. Open **SEO → Redirects** and select **Import / Export**.

### File Format

Imports accept CSV or JSON:

* CSV files require a header row. Header names are normalized to lowercase snake case.
* JSON files must contain an array of objects.

| Column                  | Required           | Default       | Description                                                                                                               |
| ----------------------- | ------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `source`                | Yes                | —             | Exact path, wildcard, or regex.                                                                                           |
| `destination`           | Unless using `410` | —             | Relative path, full URL, or `entry::<id>` reference. CSV files always need the column, but `410` rows may leave it blank. |
| `response_code`         | No                 | `301`         | `301`, `302`, or `410`.                                                                                                   |
| `preserve_query_string` | No                 | `true`        | Whether to forward the query string. Ignored for `410`.                                                                   |
| `site`                  | Multi-site only    | Selected site | The Statamic site handle.                                                                                                 |
| `enabled`               | No                 | `true`        | Whether the redirect is active.                                                                                           |
| `description`           | No                 | Empty         | An internal note.                                                                                                         |

Imported entry references must point to an existing, published entry.

{% tabs %}
{% tab title="CSV" %}

```csv
source,destination,response_code,preserve_query_string,site,enabled,description
/old-page,/new-page,301,true,default,true,Legacy page
/blog/*,/articles/$1,301,true,default,true,
/removed,,410,,default,true,Content removed
```

{% endtab %}

{% tab title="JSON" %}

```json
[
  {
    "source": "/old-page",
    "destination": "/new-page",
    "response_code": 301,
    "preserve_query_string": true,
    "site": "default",
    "enabled": true,
    "description": "Legacy page"
  },
  {
    "source": "/removed",
    "response_code": 410,
    "site": "default"
  }
]
```

{% endtab %}
{% endtabs %}

### Import Behavior

Imports are all-or-nothing. Every row is validated before anything is saved. If one row is invalid, no redirects are imported.

A row with the same source and site as an existing redirect updates that redirect. The imported row completely replaces its editable values, so omitted optional columns reset to their documented defaults rather than preserving old values.

Imported redirects are marked with the `Import` origin. Duplicate source and site combinations within the same file are rejected. Users may only import redirects for sites they are authorized to access.

### Export Behavior

Export redirects as CSV or JSON. You can export all redirects or only redirects matching the listing's current search and filters.

Exports contain the same columns accepted by the importer. Hit statistics, creation dates, and origins are not included. Only redirects from sites the current user can access are exported.


# Social Images Generator

Automatically generate social images for your entries and taxonomy terms using customizable themes.

{% hint style="info" %}
The social images generator requires the [Pro edition](/getting-started/editions).
{% endhint %}

## Setup

Run the install command and select **Social Images Generator**:

```shell
php please seo:install
```

This installs the required dependencies, enables the feature in your config, and creates a default theme.

## Requirements

The generator uses [spatie/laravel-screenshot](https://spatie.be/docs/laravel-screenshot/v1/introduction) under the hood, which supports two screenshot drivers:

* **Browsershot** (default) - Requires [Puppeteer](https://github.com/puppeteer/puppeteer) installed on your server.
* **Cloudflare Browser Rendering** — A cloud-based alternative that doesn't require Puppeteer.

The install command lets you choose your driver and handles the setup. To configure the screenshot driver manually, publish the config:

```shell
php artisan vendor:publish --tag=laravel-screenshot-config
```

{% tabs %}
{% tab title="Browsershot (default)" %}
Browsershot requires [Puppeteer](https://github.com/puppeteer/puppeteer) installed on your server. Refer to the [Browsershot documentation](https://spatie.be/docs/browsershot/v5/requirements) for setup instructions.
{% endtab %}

{% tab title="Cloudflare Browser Rendering" %}
Cloudflare Browser Rendering is a cloud-based alternative that doesn't require Puppeteer. Configure the driver in your published `config/screenshot.php`:

```php
'driver' => 'cloudflare',

'cloudflare' => [
    'account_id' => env('CLOUDFLARE_ACCOUNT_ID'),
    'api_token' => env('CLOUDFLARE_API_TOKEN'),
],
```

Refer to the [spatie/laravel-screenshot documentation](https://spatie.be/docs/laravel-screenshot/v1/introduction) for more details.
{% endtab %}
{% endtabs %}

## Enable Generator

{% stepper %}
{% step %}

#### Enable globally

The install command enables this automatically. To enable it manually, set the following in your config:

```php
// config/advanced-seo.php
'social_images' => [
    'generator' => [
        'enabled' => true,
    ],
],
```

{% endstep %}

{% step %}

#### Enable per collection/taxonomy

Head to the SEO section in the Control Panel. Click the **Configure** button on the collection or taxonomy you want to enable the generator for, and toggle **Social Images Generator** on.
{% endstep %}
{% endstepper %}

{% hint style="info" %}
The generator supports both entries and taxonomy terms.
{% endhint %}

## Generating Images

### On Save

Images are automatically generated when you save an entry or term. Smart regeneration ensures images are only regenerated when content actually changes. Saving without content changes won't trigger regeneration.

Generation runs asynchronously after the response is sent. For better performance, configure a queue:

```php
'social_images' => [
    'generator' => [
        'queue' => 'default',
    ],
],
```

### On Demand

If a generated image is missing (e.g. accidentally deleted), it will be regenerated on-the-fly on the next frontend request.

### Action

You can generate images at any time using the action in the collection or taxonomy listing view.

### Command

Generate social images for all entries and terms at once:

```shell
php please seo:generate-images
```

## Unified Social Image

The generator produces a single Open Graph image that is shared between Open Graph and X (Twitter) meta tags. The **Twitter Card** setting on each [collection/taxonomy configuration](/usage/settings-and-defaults#configuration) determines which preset is used to resize the shared image for the Twitter meta tags.

## Themes

Themes are Antlers templates that define the visual design of your generated social images. You can create multiple themes and switch between them in the publish form.

### Creating a Theme

Run the following command to create a new theme:

```shell
php please seo:theme {name}
```

This publishes a default layout and an `open_graph.antlers.html` template:

```
resources/views/social_images/
├── layout.antlers.html
└── {name}/
    └── open_graph.antlers.html
```

### Template Structure

Each theme needs an `open_graph.antlers.html` template. Design your template like any Statamic view, using the full power of Antlers: variables, tags, partials, and any styling approach you prefer.

The template receives all of the entry or term's augmented data as variables.

### Previewing

You can preview your templates in the browser using this URL pattern:

```
https://site.test/!/advanced-seo/social-images/{theme}/open-graph/{id}/{site}
```

| Variable | Description          | Example                                |
| -------- | -------------------- | -------------------------------------- |
| `theme`  | The theme handle     | `default`                              |
| `id`     | The entry or term ID | `4358df35-c7fe-4774-97ad-02af0e2dea3b` |
| `site`   | The site handle      | `default`                              |

### Theme Restrictions

You can restrict which themes are available for a specific collection or taxonomy in its [configuration](/usage/settings-and-defaults#configuration). When themes are restricted, only the selected themes appear in the theme dropdown on entries and terms.

### Inline Preview

The social preview in the publish form renders the template used to generate the social image. It updates in real time as you edit the entry or term. When multiple themes are available, you can switch between them to see how each looks before saving.


# Tokens

Register custom token normalizers for additional fieldtypes or add custom value tokens to the token input.

{% hint style="info" %}
Custom tokens require the [Pro edition](/getting-started/editions).
{% endhint %}

The [token input](/usage/on-page-seo#token-input) can be extended in two ways:

* **Token Normalizers** — Convert field values into strings so they can be used as tokens.
* **Value Tokens** — Tokens that provide static or dynamic values.

## Token Normalizers

Token normalizers convert field values into strings so they can be used as tokens. Advanced SEO includes normalizers for common fieldtypes like Text, Bard, and Markdown. If you need support for a fieldtype that isn't covered, create a normalizer for it.

A normalizer requires both a PHP class for server-side rendering and a JavaScript normalizer for the live preview in the Control Panel.

### Creating a Normalizer

Extend the `TokenNormalizer` class and implement the `fieldtype()` and `normalize()` methods:

```php
namespace App\Seo\Tokens;

use Aerni\AdvancedSeo\Tokens\TokenNormalizer;
use Statamic\Fields\Value;

class TagsTokenNormalizer extends TokenNormalizer
{
    public function fieldtype(): string
    {
        return 'tags';
    }

    public function normalize(Value $value): ?string
    {
        return collect($value->value())->join(', ');
    }
}
```

Register a JavaScript normalizer for the same fieldtype handle using `Statamic.$advancedSeo.normalizers.add()`. The normalizer receives the raw field value and field meta:

```js
Statamic.booting(() => {
    Statamic.$advancedSeo.normalizers.add('tags', (value, meta) => {
        return (value ?? []).join(', ');
    });
});
```

### API

| Method                             | Description                                                            |
| ---------------------------------- | ---------------------------------------------------------------------- |
| `fieldtype(): string`              | Return the handle of the fieldtype this normalizer is for              |
| `normalize(Value $value): ?string` | Convert the field value to a plain string so it can be used as a token |

<details>

<summary>Built-in normalizers for reference</summary>

| Normalizer                | Fieldtype  | Behavior                                         |
| ------------------------- | ---------- | ------------------------------------------------ |
| `TextTokenNormalizer`     | `text`     | Strips tags and trims                            |
| `TextareaTokenNormalizer` | `textarea` | Strips tags and trims                            |
| `BardTokenNormalizer`     | `bard`     | Converts Bard content to plain text              |
| `MarkdownTokenNormalizer` | `markdown` | Converts Markdown to plain text                  |
| `UsersTokenNormalizer`    | `users`    | Resolves user names (comma-separated with "and") |

</details>

## Value Tokens

Value tokens provide static or dynamic values that appear alongside field tokens in the autocomplete. They are useful for values that don't come from a blueprint field.

### Creating a Value Token

Extend the `ValueToken` class and implement the `handle()` and `value()` methods. The handle is used as the Antlers tag name. A handle of `company_name` becomes `{{ company_name }}` in the token input:

```php
namespace App\Seo\Tokens;

use Aerni\AdvancedSeo\Tokens\ValueToken;

class CompanyNameToken extends ValueToken
{
    public function handle(): string
    {
        return 'company_name';
    }

    public function value(): ?string
    {
        return config('app.company_name');
    }
}
```

The `$this->parent` property gives you access to the entry or term the token is being resolved for, enabling context-aware values.

### API

| Method              | Description                                                                                         |
| ------------------- | --------------------------------------------------------------------------------------------------- |
| `handle(): string`  | Return the token handle, used as the Antlers tag name                                               |
| `value(): ?string`  | Return the token's value                                                                            |
| `display(): string` | Return the display label shown in the autocomplete. Defaults to a title-cased version of the handle |
| `group(): string`   | Return the group name for the autocomplete dropdown. Defaults to "Common"                           |

<details>

<summary>Built-in value tokens for reference</summary>

| Token            | Handle      | Value                          |
| ---------------- | ----------- | ------------------------------ |
| `SeparatorToken` | `separator` | The configured title separator |
| `SiteNameToken`  | `site_name` | The configured site name       |

</details>

## Registration

Both token normalizers and value tokens can be registered in two ways:

### Via Config

Add the class to the `tokens` array in `config/advanced-seo.php`:

```php
'tokens' => [
    App\Seo\Tokens\TagsTokenNormalizer::class,
    App\Seo\Tokens\CompanyNameToken::class,
],
```

### Via Service Provider

Register the class in a service provider's `boot` method:

```php
public function boot(): void
{
    \App\Seo\Tokens\TagsTokenNormalizer::register();
    \App\Seo\Tokens\CompanyNameToken::register();
}
```


# Sitemaps

Register custom sitemaps to add any Statamic or Laravel route to Advanced SEO's sitemap index.

{% hint style="info" %}
Custom sitemaps require the [Pro edition](/getting-started/editions).
{% endhint %}

Custom sitemaps let you add any route to Advanced SEO's sitemaps. Each custom sitemap declares which site it belongs to, and the sitemap index automatically includes it when generating sitemaps for that site's domain.

## Class-Based Registration

Create a dedicated class that extends `CustomSitemap`. This is the preferred approach as it keeps your service provider clean:

```php
namespace App\Sitemaps;

use Aerni\AdvancedSeo\Sitemaps\Custom\CustomSitemap;
use Illuminate\Support\Collection;

class MarketingSitemap extends CustomSitemap
{
    protected string $handle = 'marketing';

    protected string $site = 'english';

    public function urls(): Collection
    {
        return collect([
            $this->makeUrl('https://example.com/sign-in'),
            $this->makeUrl('https://example.com/pricing')
                ->lastmod(now())
                ->changefreq('weekly')
                ->priority('0.8'),
        ]);
    }
}
```

The `$site` property determines which site's sitemap index the custom sitemap belongs to. If not set, it defaults to the default Statamic site.

Class-based sitemaps can be registered in two ways:

### Via Config

Add the class to the `custom` array in `config/advanced-seo.php`:

```php
'sitemap' => [
    'custom' => [
        App\Sitemaps\MarketingSitemap::class,
    ],
],
```

### Via Service Provider

Register the class in a service provider's `boot` method:

```php
public function boot(): void
{
    \App\Sitemaps\MarketingSitemap::register();
}
```

## Inline Registration

For simpler sitemaps, you can use the `Sitemap` facade to build and register a custom sitemap inline:

```php
use Aerni\AdvancedSeo\Facades\Sitemap;
use Aerni\AdvancedSeo\Sitemaps\Custom\CustomSitemapUrl;

public function boot(): void
{
    Sitemap::make('marketing')
        ->site('english')
        ->add('https://example.com/sign-in')
        ->add('https://example.com/pricing', function (CustomSitemapUrl $url) {
            $url->lastmod(now())
                ->changefreq('weekly')
                ->priority('0.8')
                ->alternates([
                    ['href' => 'https://example.com/pricing', 'hreflang' => 'en'],
                    ['href' => 'https://example.com/de/pricing', 'hreflang' => 'de'],
                ]);
        })
        ->register();
}
```

## API Reference

### SitemapBuilder

All setter methods are fluent and return the builder instance:

| Method                   | Description                                                                    |
| ------------------------ | ------------------------------------------------------------------------------ |
| `->site($site)`          | Set the site handle (defaults to the default Statamic site)                    |
| `->add($url, $callback)` | Add a URL string with an optional callback to configure the `CustomSitemapUrl` |
| `->register()`           | Register this sitemap (must be called last)                                    |

### CustomSitemapUrl

All setter methods are fluent and return the URL instance:

| Method                  | Description            | Default | Values                                                              |
| ----------------------- | ---------------------- | ------- | ------------------------------------------------------------------- |
| `->lastmod($carbon)`    | Last modification date | `now()` | A `Carbon` instance                                                 |
| `->changefreq($freq)`   | Change frequency       | `null`  | `always`, `hourly`, `daily`, `weekly`, `monthly`, `yearly`, `never` |
| `->priority($priority)` | Priority               | `null`  | `0.0` to `1.0` in `0.1` increments                                  |
| `->alternates($array)`  | Hreflang alternates    | `null`  | Array of `['href' => ..., 'hreflang' => ...]`                       |


# Custom Routes

Add SEO support to custom Statamic or Laravel routes without reinventing the SEO implementation.

{% hint style="info" %}
Custom routes require the [Pro edition](/getting-started/editions).
{% endhint %}

## Seo::data()

Use the `Seo::data()` fluent builder to add SEO metadata to custom routes:

```php
use Aerni\AdvancedSeo\Facades\Seo;

Route::statamic('/login', 'login', [
    'title' => 'Login',
    ...Seo::data()
        ->title('Login')
        ->description('Welcome back! Login to your account.')
        ->toArray(),
]);
```

This also works with regular Laravel routes:

```php
Route::get('/login', function () {
    return view('login', [
        'title' => 'Login',
        ...Seo::data()
            ->title('Login')
            ->description('Welcome back! Login to your account.')
            ->toArray(),
    ]);
});
```

### Available Methods

| Method                   | Description                                                                                   |
| ------------------------ | --------------------------------------------------------------------------------------------- |
| `title(string)`          | Set the meta title.                                                                           |
| `description(string)`    | Set the meta description.                                                                     |
| `ogTitle(string)`        | Set the Open Graph title.                                                                     |
| `ogDescription(string)`  | Set the Open Graph description.                                                               |
| `ogImage(string\|Asset)` | Set the Open Graph image. Accepts a URL or Statamic Asset.                                    |
| `noindex()`              | Mark the page as noindex.                                                                     |
| `nofollow()`             | Mark the page as nofollow.                                                                    |
| `canonicalUrl(string)`   | Set a custom canonical URL.                                                                   |
| `jsonLd(string\|Type)`   | Set JSON-LD structured data. Accepts a raw JSON string or a `Spatie\SchemaOrg\Type` instance. |


# Events

Listen to Advanced SEO events to react to changes in SEO configuration and content data.

Advanced SEO dispatches events when SEO data is saved or deleted. All events are in the `Aerni\AdvancedSeo\Events` namespace.

## Available Events

### SeoSetConfigSaved

`Aerni\AdvancedSeo\Events\SeoSetConfigSaved`

Dispatched after a set's configuration has been saved.

```php
public function handle(SeoSetConfigSaved $event)
{
    $event->config;
}
```

### SeoSetConfigDeleted

`Aerni\AdvancedSeo\Events\SeoSetConfigDeleted`

Dispatched after a set's configuration has been deleted.

```php
public function handle(SeoSetConfigDeleted $event)
{
    $event->config;
}
```

### SeoSetLocalizationSaved

`Aerni\AdvancedSeo\Events\SeoSetLocalizationSaved`

Dispatched after a localization has been saved.

```php
public function handle(SeoSetLocalizationSaved $event)
{
    $event->localization;
}
```

### SeoSetLocalizationDeleted

`Aerni\AdvancedSeo\Events\SeoSetLocalizationDeleted`

Dispatched after a localization has been deleted.

```php
public function handle(SeoSetLocalizationDeleted $event)
{
    $event->localization;
}
```


# Antlers & Blade

Access SEO values in your Antlers and Blade views.

## Accessing Variables

All the SEO values are processed and added to your views in an array with key `seo`. This array includes all the SEO data of the current entry or term, as well as global site defaults and computed values.

You may access any of those values like you would with any other value in Antlers or Blade:

```html
<!-- Antlers -->
{{ seo:title }}

<!-- Blade -->
{{ $seo->title }}
```

## Antlers Tags & Blade Directives

There are a couple of tags and directives to use in your views:

| Antlers Tag      | Blade Directive | Description                               |
| ---------------- | --------------- | ----------------------------------------- |
| `{{ seo:head }}` | `@seo('head')`  | Render the head view                      |
| `{{ seo:body }}` | `@seo('body')`  | Render the body view                      |
| `{{ seo:dump }}` | `@seo('dump')`  | Dump all the SEO data of the current page |


# GraphQL

Query SEO metadata, defaults, and sitemaps using Advanced SEO's GraphQL API.

{% hint style="info" %}
The GraphQL API requires the [Pro edition](/getting-started/editions).
{% endhint %}

## Enable GraphQL

Enable the GraphQL API in `config/advanced-seo.php`:

```php
'graphql' => true,
```

{% hint style="info" %}
Statamic's GraphQL API must also be enabled. Refer to the [Statamic documentation](https://statamic.dev/graphql) for setup.
{% endhint %}

{% hint style="info" %}
Fields belonging to disabled features are removed from the schema. For example, disabling the sitemap removes all sitemap-related fields, and disabling Fathom Analytics removes the Fathom fields.
{% endhint %}

## Queries

### seoMeta

Query the SEO metadata of a specific entry or term.

Returns a [seoMeta](#seometa) type.

| Argument | Type      | Description                                                                                                                                   |
| -------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`     | `String!` | The entry or term ID.                                                                                                                         |
| `site`   | `String`  | Site handle. Without it, returns the entry or term in its origin localization. Returns `null` if the content doesn't exist in the given site. |

```graphql
{
  seoMeta(id: "entry-uuid-here", site: "default") {
    computed {
      canonical
      indexing
      locale
    }
    raw {
      title
      description
    }
    view {
      head
      body
    }
  }
}
```

See the [seoMeta](#seometa) type for all available fields.

### seoSet

Query the SEO defaults for the site, collections, and taxonomies.

Returns a [seoSet](#seoset) type.

```graphql
{
  seoSet {
    site(site: "default") {
      site_name
      separator
    }
    collection(handle: "pages", site: "default") {
      title
      description
    }
    taxonomy(handle: "tags", site: "default") {
      title
      description
    }
  }
}
```

See the [siteSet](#siteset), [collectionSet](#collectionset), and [taxonomySet](#taxonomyset) types for all available fields.

### seoSitemaps

Query the sitemaps for a specific site.

Returns a list of [Sitemap](#sitemap) types.

| Argument | Type          | Description                                                             |
| -------- | ------------- | ----------------------------------------------------------------------- |
| `site`   | `String!`     | Site handle. Returns sitemaps for all sites sharing that site's domain. |
| `type`   | `SitemapType` | Filter by sitemap type: `COLLECTION`, `TAXONOMY`, or `CUSTOM`.          |
| `handle` | `String`      | Filter by collection, taxonomy, or custom sitemap handle.               |

```graphql
{
  seoSitemaps(site: "default", type: COLLECTION, handle: "pages") {
    id
    type
    handle
    lastmod
    urls {
      loc
      lastmod
      changefreq
      priority
      alternates { href, hreflang }
    }
  }
}
```

## Fields

### seo

When GraphQL is enabled, an `seo` field is added to the `EntryInterface` and `TermInterface`. It returns a [seoMeta](#seometa) type.

```graphql
{
  entries {
    data {
      title
      seo {
        computed {
          canonical
          indexing
        }
        raw {
          title
          description
        }
      }
    }
  }
}
```

## Types

### seoMeta

| Field      | Type                                  | Description                                         |
| ---------- | ------------------------------------- | --------------------------------------------------- |
| `computed` | [computedMetaData](#computedmetadata) | Resolved values after cascade processing.           |
| `raw`      | [rawMetaData](#rawmetadata)           | Direct field values as stored on the entry or term. |
| `view`     | [renderedViews](#renderedviews)       | Pre-rendered HTML for the head and body.            |

### computedMetaData

| Field                  | Type                                    | Description                                         |
| ---------------------- | --------------------------------------- | --------------------------------------------------- |
| `canonical`            | `String`                                | The canonical URL.                                  |
| `indexing`             | `String`                                | The indexing directive (e.g. `noindex, nofollow`).  |
| `locale`               | `String`                                | The current locale.                                 |
| `twitter_card`         | `String`                                | The card type (`summary` or `summary_large_image`). |
| `twitter_handle`       | `String`                                | The X (Twitter) handle.                             |
| `hreflang`             | \[[Hreflang](#hreflang)]                | Alternate language versions.                        |
| `og_image_preset`      | [SocialImagePreset](#socialimagepreset) | The OG image dimensions.                            |
| `twitter_image_preset` | [SocialImagePreset](#socialimagepreset) | The Twitter image dimensions.                       |
| `site_schema`          | `String`                                | The site JSON-LD schema.                            |
| `breadcrumbs`          | `String`                                | The JSON-LD breadcrumbs.                            |

### rawMetaData

The raw field values as stored on the entry or term. Field handles match the blueprint with the `seo_` prefix removed.

| Field                    | Type      | Description                                               |
| ------------------------ | --------- | --------------------------------------------------------- |
| `title`                  | `String`  | The meta title.                                           |
| `description`            | `String`  | The meta description.                                     |
| `og_title`               | `String`  | The Open Graph title.                                     |
| `og_description`         | `String`  | The Open Graph description.                               |
| `og_image`               | `String`  | The Open Graph image.                                     |
| `generate_social_images` | `Boolean` | Whether to generate social images.                        |
| `social_images_theme`    | `String`  | The social images theme.                                  |
| `noindex`                | `Boolean` | Whether the entry is noindexed.                           |
| `nofollow`               | `Boolean` | Whether the entry is nofollowed.                          |
| `canonical_type`         | `String`  | The canonical URL type (`current`, `entry`, or `custom`). |
| `canonical_entry`        | `String`  | The canonical entry ID.                                   |
| `canonical_custom`       | `String`  | The custom canonical URL.                                 |
| `sitemap_enabled`        | `Boolean` | Whether the entry is included in the sitemap.             |
| `json_ld`                | `String`  | The custom JSON-LD.                                       |

### renderedViews

Pre-rendered HTML for the `<head>` and `<body>` sections. Only use this when your frontend is hosted on the same domain as Statamic, as the views contain absolute URLs.

| Field  | Type     | Description             |
| ------ | -------- | ----------------------- |
| `head` | `String` | The rendered head HTML. |
| `body` | `String` | The rendered body HTML. |

### seoSet

| Field        | Type                            | Arguments                         | Description                                          |
| ------------ | ------------------------------- | --------------------------------- | ---------------------------------------------------- |
| `site`       | [siteSet](#siteset)             | `site: String`                    | Site-wide defaults. Falls back to the default site.  |
| `collection` | [collectionSet](#collectionset) | `handle: String!`, `site: String` | Collection defaults. Falls back to the default site. |
| `taxonomy`   | [taxonomySet](#taxonomyset)     | `handle: String!`, `site: String` | Taxonomy defaults. Falls back to the default site.   |

### siteSet

The site-wide SEO defaults. Field handles match the site blueprint.

| Field                           | Type      | Description                                                       |
| ------------------------------- | --------- | ----------------------------------------------------------------- |
| `site_name`                     | `String`  | The site name.                                                    |
| `separator`                     | `String`  | The title separator.                                              |
| `site_json_ld_type`             | `String`  | The JSON-LD type (`none`, `organization`, `person`, or `custom`). |
| `use_breadcrumbs`               | `Boolean` | Whether to use breadcrumbs.                                       |
| `organization_name`             | `String`  | The organization name.                                            |
| `organization_logo`             | `String`  | The organization logo.                                            |
| `person_name`                   | `String`  | The person name.                                                  |
| `site_json_ld`                  | `String`  | The custom site JSON-LD.                                          |
| `favicon_svg`                   | `String`  | The SVG favicon.                                                  |
| `og_image`                      | `String`  | The default Open Graph image.                                     |
| `twitter_card`                  | `String`  | The card type (`summary` or `summary_large_image`).               |
| `twitter_handle`                | `String`  | The X (Twitter) handle.                                           |
| `noindex`                       | `Boolean` | Whether the site is noindexed.                                    |
| `google_site_verification_code` | `String`  | The Google site verification code.                                |
| `bing_site_verification_code`   | `String`  | The Bing site verification code.                                  |
| `fathom_id`                     | `String`  | The Fathom site ID.                                               |
| `fathom_spa`                    | `Boolean` | Whether to enable Fathom SPA mode.                                |
| `cloudflare_beacon_token`       | `String`  | The Cloudflare Web Analytics beacon token.                        |
| `gtm_container_id`              | `String`  | The Google Tag Manager container ID.                              |

### collectionSet

The SEO defaults for a collection. Field handles match the blueprint with the `seo_` prefix removed.

| Field                    | Type      | Description                                  |
| ------------------------ | --------- | -------------------------------------------- |
| `title`                  | `String`  | The default meta title.                      |
| `description`            | `String`  | The default meta description.                |
| `og_title`               | `String`  | The default Open Graph title.                |
| `og_description`         | `String`  | The default Open Graph description.          |
| `og_image`               | `String`  | The default Open Graph image.                |
| `generate_social_images` | `Boolean` | Whether to generate social images.           |
| `social_images_theme`    | `String`  | The social images theme.                     |
| `noindex`                | `Boolean` | Whether to noindex entries.                  |
| `nofollow`               | `Boolean` | Whether to nofollow entries.                 |
| `sitemap_enabled`        | `Boolean` | Whether entries are included in the sitemap. |
| `json_ld`                | `String`  | The default JSON-LD.                         |

### taxonomySet

The SEO defaults for a taxonomy. Has the same fields as [collectionSet](#collectionset).

### Sitemap

| Field     | Type                         | Description                                               |
| --------- | ---------------------------- | --------------------------------------------------------- |
| `id`      | `String`                     | The sitemap identifier (e.g. `collection-pages`).         |
| `type`    | `String`                     | The sitemap type (`collection`, `taxonomy`, or `custom`). |
| `handle`  | `String`                     | The collection, taxonomy, or custom sitemap handle.       |
| `lastmod` | `String`                     | The last modification date.                               |
| `urls`    | \[[SitemapUrl](#sitemapurl)] | The URLs in the sitemap.                                  |

### SitemapUrl

| Field        | Type                                       | Description                                                                       |
| ------------ | ------------------------------------------ | --------------------------------------------------------------------------------- |
| `loc`        | `String`                                   | The URL location.                                                                 |
| `lastmod`    | `String`                                   | The last modification date.                                                       |
| `changefreq` | `String`                                   | The change frequency. Only present for custom sitemaps that set this value.       |
| `priority`   | `String`                                   | The priority (`0.0`–`1.0`). Only present for custom sitemaps that set this value. |
| `alternates` | \[[SitemapAlternates](#sitemapalternates)] | Hreflang alternates.                                                              |

### SitemapAlternates

| Field      | Type     | Description        |
| ---------- | -------- | ------------------ |
| `href`     | `String` | The alternate URL. |
| `hreflang` | `String` | The language code. |

### Hreflang

| Field    | Type     | Description                 |
| -------- | -------- | --------------------------- |
| `url`    | `String` | The alternate language URL. |
| `locale` | `String` | The language locale code.   |

### SocialImagePreset

| Field    | Type     | Description       |
| -------- | -------- | ----------------- |
| `width`  | `String` | The image width.  |
| `height` | `String` | The image height. |


# Meta Tags & Scripts

Advanced SEO supports all of the following meta tags and scripts.

### Basic

* `title`
* `description`
* `alternate (hreflang)`
* `canonical`
* `prev`
* `next`
* `robots (noindex, nofollow)`
* `icon (Favicon)`

### Open Graph

* `og:type`
* `og:site_name`
* `og:locale`
* `og:locale:alternate`
* `og:title`
* `og:description`
* `og:url`
* `og:image`
* `og:image:width`
* `og:image:height`

### X (Twitter)

* `twitter:card`
* `twitter:title`
* `twitter:description`
* `twitter:site`
* `twitter:image`
* `twitter:image:alt`

{% hint style="info" %}
The title, description, image, and image alt text are populated from their Open Graph counterparts. Only the card type and site handle are Twitter-specific settings.
{% endhint %}

### Site Verification

* `google-site-verification`
* `msvalidate.01 (Bing)`

### Knowledge Graph

* `JSON-LD Site Schema`
* `JSON-LD Page Schema`
* `JSON-LD Breadcrumbs`

### Analytics

* `Fathom`
* `Cloudflare Web Analytics`
* `Google Tag Manager`


