SEO meta tags
meta_title and meta_description are translatable columns; every other SEO field lives in a non-translatable seo bag on the post. Store them with Posts::seo() and the SeoData DTO:
use RoundlyConsulting\Posts\DataTransferObjects\SeoData;
use RoundlyConsulting\Posts\Facades\Posts;
Posts::seo($post, new SeoData( // saves; $post->setSeo($data) only sets the attributes
metaTitle: 'Custom title',
canonical: 'https://example.com/posts/hello-world',
ogImage: 'https://example.com/cover.png',
robots: 'index,follow',
));Posts::seo() calls the model’s setSeo() and saves. setSeo() writes a non-null metaTitle / metaDescription as the current locale’s translation and replaces the whole seo bag with the DTO’s other fields — pass every field you want to keep. On its own it does not save:
// The model form sets the attributes without saving:
$post->setSeo(new SeoData(ogImage: 'https://example.com/cover.jpg'));
$post->save();Rendering the tags
<head>
{!! $post->renderMetaTags() !!} {{-- <title>, description, canonical, og:*, twitter:*, robots --}}
{!! $post->renderJsonLd() !!} {{-- <script type="application/ld+json"> BlogPosting/Article --}}
</head>renderMetaTags() renders the posts::meta view and emits only the tags that have a value, each one escaped:
<title>Custom title</title>
<meta name="description" content="A short summary">
<meta name="robots" content="index,follow">
<link rel="canonical" href="https://example.com/posts/hello-world">
<meta property="og:title" content="Custom title">
<meta property="og:description" content="A short summary">
<meta property="og:type" content="article">
<meta property="og:image" content="https://example.com/cover.png">
<meta property="og:site_name" content="Example Blog">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@example">Fallbacks
seo() returns a SeoData with config defaults and fallbacks merged. The meta fields use the same locale fallback as $post->title: the current locale’s meta value or title first, then the fallback locale’s, then any locale’s — so a post that only has an English title still renders a <title> and a JSON-LD headline on a Slovak page:
$seo = $post->seo(); // SeoData with config defaults + fallbacks merged
$seo->metaTitle; // meta_title → title, current locale → fallback → any locale
$seo->metaDescription; // meta_description → perex, same locale chain
$seo->ogImage; // explicit og:image → featured image URL → null
$seo->twitterCard; // explicit → posts.seo.default-card| Field | Resolved from |
|---|---|
metaTitle | meta_title → title in the current locale, then the same pair in the fallback locale, then any locale → null |
metaDescription | meta_description → perex, along the same locale chain → null |
canonical | stored value only |
twitterCreator | stored value only |
ogTitle | stored value → metaTitle |
ogDescription | stored value → metaDescription |
ogImage | stored value → featured image URL at media.og_variant once generated, else the original (when media.seo_og_image) → null |
ogType | stored value → article |
twitterCard | stored value → seo.default-card |
twitterSite | stored value → seo.twitter-site |
robots | stored value → seo.default-robots |
ogSiteName | stored value → seo.site-name |
Restyling
Publish posts-views and edit meta.blade.php, which receives $seo (a SeoData), to change the markup.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.