NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Posts for Laravel

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
FieldResolved from
metaTitlemeta_title → title in the current locale, then the same pair in the fallback locale, then any locale → null
metaDescriptionmeta_description → perex, along the same locale chain → null
canonicalstored value only
twitterCreatorstored value only
ogTitlestored value → metaTitle
ogDescriptionstored value → metaDescription
ogImagestored value → featured image URL at media.og_variant once generated, else the original (when media.seo_og_image) → null
ogTypestored value → article
twitterCardstored value → seo.default-card
twitterSitestored value → seo.twitter-site
robotsstored value → seo.default-robots
ogSiteNamestored 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 crypto

By 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.