---
title: "SEO meta tags — Posts for Laravel | Roundly"
description: "Per-post meta, canonical, Open Graph and Twitter tags via SeoData, with config defaults and fallbacks from the post itself."
url: https://roundly-consulting.com/open-source/docs/posts-for-laravel/seo
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Posts for Laravel](https://roundly-consulting.com/open-source/docs/posts-for-laravel.md)

# 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:

```php
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:

```php
// The model form sets the attributes without saving:
$post->setSeo(new SeoData(ogImage: 'https://example.com/cover.jpg'));
$post->save();
```

## Rendering the tags

```blade
<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:

```html
<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:

```php
$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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

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

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/posts-for-laravel.md)
