Technical SEO for Laravel Blogs: JSON-LD and Open Graph
In the file-based Markdown blog ecosystem, content is king, but metadata is the crown. Without proper structured data and meta tags, even the best content can get lost in the search engine shuffle.
This guide focuses on implementing Technical SEO for a Laravel blog, ensuring your content looks great on Google search results and social media shares.
The Strategy
We will focus on three key areas:
- Standard Meta Tags: Title, description, canonical URLs.
- Open Graph (OG) & Twitter Cards: For social media previews.
- JSON-LD Structured Data: Helping search engines understand your content (Article, Breadcrumb).
1. The Head Component
Instead of cluttering your layout.blade.php, create a dedicated SEO component.
php artisan make:component SeoMetadata
In resources/views/components/seo-metadata.blade.php:
@props(['post' => null, 'title' => 'My Tech Blog', 'description' => 'A blog about Laravel and code.'])
@php
$pageTitle = $post ? $post->title . ' | My Tech Blog' : $title;
$pageDescription = $post ? $post->description : $description;
$currentUrl = url()->current();
$ogImage = $post && $post->cover ? asset('images/posts/' . $post->slug . '/' . $post->cover) : asset('images/default-og.jpg');
@endphp
<title>{{ $pageTitle }}</title>
<meta name="description" content="{{ $pageDescription }}">
<link rel="canonical" href="{{ $currentUrl }}">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="{{ $post ? 'article' : 'website' }}">
<meta property="og:url" content="{{ $currentUrl }}">
<meta property="og:title" content="{{ $pageTitle }}">
<meta property="og:description" content="{{ $pageDescription }}">
<meta property="og:image" content="{{ $ogImage }}">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="{{ $currentUrl }}">
<meta name="twitter:title" content="{{ $pageTitle }}">
<meta name="twitter:description" content="{{ $pageDescription }}">
<meta name="twitter:image" content="{{ $ogImage }}">
Usage in your layout:
<head>
<x-seo-metadata :post="$post ?? null" />
<!-- ... css ... -->
</head>
2. Implementing JSON-LD
JSON-LD (JavaScript Object Notation for Linked Data) is the preferred Schema.org format for Google. It allows us to explicitly tell Google: "This is a Blog Posting, written by X, published on Y."
We can add this directly to our Blade view for a single post.
@if($post)
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "{{ $post->title }}",
"image": [
"{{ asset('images/posts/' . $post->slug . '/' . $post->cover) }}"
],
"datePublished": "{{ $post->date->toIso8601String() }}",
"dateModified": "{{ $post->updated_at ? $post->updated_at->toIso8601String() : $post->date->toIso8601String() }}",
"author": [{
"@type": "Person",
"name": "Your Name",
"url": "{{ url('/') }}"
}],
"publisher": {
"@type": "Organization",
"name": "My Tech Blog",
"logo": {
"@type": "ImageObject",
"url": "{{ asset('images/logo.png') }}"
}
},
"description": "{{ $post->description }}"
}
</script>
@endif
Why Dynamic Rendering?
Since our content is static Markdown, we need to pass these values into the view dynamically. Using Carbon for dates ensures the correct ISO 8601 format required by Google.
3. Dynamic Sitemap
A sitemap.xml is crucial for letting crawlers discover your URLs. Since we are using a file-based system, we can generate this on the fly or cache it.
Route definition:
Route::get('/sitemap.xml', function () {
$posts = \App\Facades\Markdown::getAllPosts();
return response()->view('sitemap', [
'posts' => $posts
])->header('Content-Type', 'text/xml');
});
The view resources/views/sitemap.blade.php:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>{{ url('/') }}</loc>
<lastmod>{{ now()->toAtomString() }}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
@foreach ($posts as $post)
<url>
<loc>{{ route('blog.show', $post->slug) }}</loc>
<lastmod>{{ $post->date->toAtomString() }}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
@endforeach
</urlset>
Testing Your Implementation
Don't guess—verify.
- Rich Results Test: Use Google's Rich Results Test tool. Paste your generated HTML or URL to verify the JSON-LD is valid.
- Meta Tag Debugger: Use Metatags.io to preview how your Twitter cards and Open Graph images look.
Summary
By abstracting meta tags into a Blade component and dynamically injecting JSON-LD, we ensure every Markdown file we create automatically gets top-tier SEO treatment without extra manual work. This is the beauty of a custom Laravel blog engine over a static site generator—you have full control over the rendering pipeline.