cnj/seotamic
Seotamic is a Laravel package for managing SEO metadata across your app. It helps you define titles, descriptions, Open Graph and Twitter card tags, and canonical URLs for pages and Eloquent models, making it easy to improve search visibility and social sharing with minimal configuration.
Install and wire it up
composer require cnj/seotamic
php artisan vendor:publish --tag=seotamic-config # inspect the tag list in the README if this misses
Statamic addons are auto-discovered, so there is no provider registration step. Open config/seotamic.php immediately — for a 21-star addon the config file is the real documentation.
Where to look first, in order
config/seotamic.php — defaults, enabled features (sitemap, OG, Twitter, robots), route/URL settings.vendor/cnj/seotamic/resources/views/ — the head-rendering partial/tag. Read it before you use it; you will almost certainly override it.vendor/cnj/seotamic/src/Fieldtypes/ — how SEO data is stored (a single seo array field vs. flat handles). This dictates your blueprint work.vendor/cnj/seotamic/routes/ — whether the sitemap/robots endpoints are registered automatically or need publishing.First use case (the 30-minute win)
Add the package's SEO fieldset to one collection, render the head partial in your layout, and confirm output in view source.
# resources/blueprints/collections/pages/pages.yaml
import:
- seotamic/seo # confirm the exact fieldset handle in the package's resources/fieldsets
{{# resources/views/layout.antlers.html #}}
<head>
<title>{{ seo:title ?? title }} — {{ site:name }}</title>
{{ seotamic }} {{# exact tag name from the README — verify #}}
</head>
Then hit a page, view source, and check that meta title, description, canonical, and OG tags are present and not HTML-escaped. Only after that works should you enable the sitemap and point Google Search Console at it.
Blueprint-per-collection with a shared fieldset
Keep the SEO fields in one fieldset and import it into every indexable collection. This is the only sane way to keep handle names consistent, because the package's rendering partial expects specific handles.
# resources/fieldsets/seo.yaml (your wrapper, not the package's)
title: SEO
fields:
-
import: seotamic/seo
If you need per-collection control (e.g. no OG image on products), wrap the import and add/remove fields locally rather than editing the vendor fieldset.
Globals for site-wide fallbacks
The common production pattern is: entry-level SEO → collection default → global default → Statamic entry title. Put the middle two in a seo_defaults global set and resolve the chain in your own partial, not in the package, so the precedence is visible in your repo:
{{ seo:meta_title
?? seo_defaults:meta_title
?? title }}
Multisite
Statamic's cascade/localization means SEO fields are per-site automatically. Two things to verify yourself: (a) whether the sitemap emits one file per site or one combined file, and (b) whether hreflang is generated. If hreflang is missing, add it manually in the layout using {{ sites }} — don't wait on the addon.
Sitemap integration
Enable the route in config, then exclude non-indexable content explicitly. The reliable pattern is to drive both <meta name="robots"> and sitemap inclusion from the same field, so a noindex entry can never appear in the sitemap:
{{ if seo:noindex }}
<meta name="robots" content="noindex,follow">
{{ /if }}
Check whether the addon already does this; if it does, do not duplicate the tag.
Rendering OG/JSON-LD safely
If the package outputs JSON-LD through an Antlers tag, confirm it is not entity-encoded. If it is, publish and override the view rather than adding |raw at the call site — |raw on a whole tag will also unescape user-supplied descriptions.
Testing
Assert on rendered HTML in a feature test. This is cheap and catches the most common failure (empty SEO fields silently rendering <title> — Site Name</title>):
$this->get('/about')
->assertSee('<meta property="og:title"', false)
->assertSee($entry->get('seo')['meta_title'], false);
It is a small package — pin it and read the source. 21 stars and zero dependents means you are the QA team. Use an exact version constraint ("cnj/seotamic": "1.2.3"), commit composer.lock, and skim the diff before every composer update. Budget an hour to read src/ end to end; it is a small addon and this is faster than debugging it blind.
Statamic v4 only. The description is explicit, and the v4→v5 fieldtype/asset API changes are exactly the kind that break SEO addons (asset container methods, Augmented interfaces). Do not assume it works on v5 — test on a branch before you upgrade Statamic.
Don't run two SEO addons. If the project already has statamic/advanced-seo or hand-rolled SEO fields, rip one out. Duplicate og:title and conflicting canonical tags are worse than either addon alone, and the conflict is invisible in the Control Panel.
Config and stache caching. After changing config/seotamic.php or blueprints:
php artisan config:clear
php artisan statamic:stache:clear
php artisan cache:clear
Blueprint changes not appearing in the CP is almost always a stale stache, not a bug in the addon.
Empty-string vs. null. Antlers ?? only falls through on null/missing, not "". A saved-but-blank SEO field will render <meta name="description" content=""> and suppress your fallback. Normalize in your partial with {{ if seo:description }}…{{ /if }} before defaulting, or use :| style trimming if the fieldtype returns a string.
OG image is an asset, not a URL. If the field is an assets fieldtype, {{ seo:og_image }} gives you an ID or an augmented asset depending on config. Use {{ seo:og_image:url }} and guard for the empty case; a missing OG image is a silent social-sharing failure that only shows up when someone shares the page.
Static caching and CDNs. If you use Statamic's static cache or a CDN in front, meta tags are baked at build time. Editing an entry's SEO fields invalidates the Statamic cache but may not purge your CDN — wire the purge into the entry saved event.
Canonical on paginated and filtered pages. Pagination (?page=2) and any query-string filtering should either canonical to the base URL or self-canonicalize deliberately. Addon defaults are usually naive here; set the canonical manually in the layout for listing pages.
Extension points to prefer. Published views and config first, custom Antlers tag second, event listener on Statamic\Events\EntrySaved third. Avoid editing vendor/ — with a package this small you will be upgrading often, and a fork means you own the maintenance.
Debugging checklist when tags don't appear: is the field actually in the blueprint for that collection → is the entry saved with values → is the tag inside <head> and not swallowed by a conditional → is the published view overriding the package view with an out-of-date signature → clear stache. That order resolves nearly every report.
Upgrade risk is front-loaded. With a 2026 last-release date and low adoption, treat minor bumps as potentially breaking. Keep the sitemap and head-rendering views overridden in your own repo so a bad release degrades to "old markup" rather than "blank <head>".
How can I help you explore Laravel packages today?