ali-awwad/fine-seo

Statamic Fine Seo addon

Maintainers

Package info

github.com/ali-awwad/fine-seo

pkg:composer/ali-awwad/fine-seo

Transparency log

Statistics

Installs: 2 954

Dependents: 0

Suggesters: 0

Stars: 5

Open Issues: 0

v0.2.2 2026-07-29 15:23 UTC

This package is auto-updated.

Last update: 2026-07-29 15:32:35 UTC


README

Fine SEO is a Statamic addon that does SEO in a simple way. The main purpose is to cover the most common scenarios.

Statamic's marketplace has other paid SEO addons with far more functionality. Fine SEO is the simple way to get started.

Requires PHP 8.3+ and Statamic 6.

Features

Per-entry SEO fields

  • A fine_seo fieldset you can import into any collection.
  • Title and description fields with live character-count indicators.
  • A preview of how the page will look in search results.

Meta output

  • <title>, meta description, Open Graph and Twitter card tags.
  • <link rel="canonical"> on every page.
  • <link rel="alternate" hreflang> across sites, including x-default.
  • JSON-LD for BreadcrumbList and WebSite.

Sitemaps

  • /sitemap.xml as a sitemap index, plus one sitemap per site.
  • hreflang alternates embedded in the sitemap as <xhtml:link>.
  • Automatically excludes drafts, redirects, scheduled and expired entries, password-protected pages, and anything without a template.
  • Per-entry "Exclude from sitemap" toggle.
  • Cached, and refreshed automatically whenever content changes.
  • A browser stylesheet, so the XML is readable if you open it.

robots.txt

  • Generated with a Sitemap: line, unless you have a real public/robots.txt (which always wins).

Installation

Search for the addon in Tools > Addons in the control panel and click install, or run:

composer require ali-awwad/fine-seo

Setup

Visit Tools > Fine SEO in the control panel and use the buttons to generate what you need:

  1. Setup Collections — writes the fine_seo fieldset and imports it into the collections you tick.
  2. Setup Global Config — creates the fine_seo_config global set (site title and separator).
  3. Setup Global Brand — creates the brand global set (title and logos).

Upgrading? The "Exclude from sitemap" toggle is added by Setup Collections. If you set your fields up before this version, run it once more to pick up the new field.

Rendering meta tags

Add the partial to the <head> of your resources/views/layout.antlers.html:

{{ partial:fine-seo::components/meta }}

That single line outputs the title, description, Open Graph and Twitter tags, canonical, hreflang alternates and the JSON-LD blocks.

Customising the output

If you don't want every tag, or you want to change the markup, copy the partial into your own project:

cp ./vendor/ali-awwad/fine-seo/resources/views/components/_meta.antlers.html \
   ./resources/views/partials/meta.antlers.html

Then call yours instead:

{{ partial:meta }}

Edit resources/views/partials/meta.antlers.html freely — it is a plain Antlers file, and the {{ fine_seo:* }} tags below keep working inside it.

Sitemaps

Nothing to configure. Once the addon is installed the routes are live:

URL What it is
/sitemap.xml Sitemap index, listing one sitemap per site
/sitemap_{site}.xml One site's URLs, e.g. /sitemap_default.xml, /sitemap_ar.xml
/sitemap.xsl Browser stylesheet (search engines ignore it)
/robots.txt Generated, with a Sitemap: line

Submit /sitemap.xml to Google Search Console. It discovers the per-site sitemaps from the index.

Excluding a page

Open any entry, go to the Fine SEO tab and switch on Exclude from sitemap.

The toggle is localizable, so on a multi-site install:

  • Set it on the original to exclude the page in every language.
  • Set it on a single translation to exclude just that one.

Either way the page is also dropped from the hreflang alternates on its translations, so the sitemap and the meta tags never contradict each other.

Clearing the cache

Sitemaps are cached and rebuilt automatically when content changes. Config or code changes don't trigger that, so clear it manually:

php please fine-seo:sitemap:clear

A note on the browser view

Chrome only pretty-prints XML that contains no HTML-namespace content. A sitemap with hreflang alternates contains <xhtml:link>, so without a stylesheet Chrome renders it as a wall of plain text — it looks broken but isn't. The bundled /sitemap.xsl avoids that. Turn it off with 'stylesheet' => false if you'd rather serve the XML bare.

Multi-site

Everything is multi-site aware out of the box: one sitemap per site, reciprocal hreflang alternates, x-default pointing at your default site, and per-site global values.

If you serve sites from subdirectories (e.g. / and /ar/), use relative URLs in resources/sites.yaml:

default:
  url: /
ar:
  url: /ar/

An absolute URL with a path (https://example.com/ar/) breaks Statamic's own {{ nav:breadcrumbs }} tag, which silently truncates the breadcrumb trail on every nested page. Absolute URLs are fine for subdomain multi-site.

Two sites sharing a short locale (en_US and en_GB both produce en) would emit duplicate hreflang values. The addon logs a warning if it detects this; switch to full locales to fix it:

'hreflang' => [
    'attribute' => 'locale', // en-US instead of en
],

Antlers tags

Available anywhere in your templates, not just in the meta partial.

{{ fine_seo:canonical }}       {{# <link rel="canonical" href="..."> #}}
{{ fine_seo:canonical_url }}   {{# just the URL #}}
{{ fine_seo:hreflang }}        {{# all <link rel="alternate" hreflang> tags #}}
{{ fine_seo:sitemap_url }}     {{# absolute URL of the sitemap index #}}

For custom markup, loop the alternates yourself:

{{ fine_seo:alternates }}
    <a href="{{ href }}" hreflang="{{ hreflang }}">{{ hreflang }}</a>
{{ /fine_seo:alternates }}

{{ fine_seo:hreflang }} and {{ fine_seo:alternates }} output nothing on a single-site install, or for a page with no translations.

Configuration

The defaults are sensible; publish the config file only if you need to change something:

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

The most useful keys in config/fine-seo.php:

'sitemap' => [
    'enabled' => true,
    'cache_ttl' => 3600,            // null disables caching
    'stylesheet' => 'sitemap.xsl',  // false to serve bare XML
    'hreflang' => [
        'enabled' => true,
        'attribute' => 'short_locale', // short_locale | locale | lang
        'x_default' => true,
    ],
    'collections' => [
        'exclude' => [],            // collection handles to leave out
    ],
    'taxonomies' => [
        'enabled' => true,
        'skip_empty_terms' => true,
        'include_archives' => false,
    ],
    'exclude_uris' => [],           // e.g. ['/search', '/campaigns/*']
],

'robots' => [
    'enabled' => true,
    'disallow' => [],
    'sitemap' => true,              // append the Sitemap: line
    'noindex_non_production' => false,
],

Read config through AliAwwad\FineSeo\Support\Config rather than config() if you extend the addon. Laravel merges package config shallowly, so a published file with a partial sitemap array would otherwise drop every nested default.

Testing

composer install
./vendor/bin/phpunit

TODOs

  • Add a YouTube video to describe this
  • Support JSON-LD for organization (WebSite is done, Organization is not)
  • Image sitemaps (<image:image>)
  • Split sitemaps over 50,000 URLs into chunks