SEO Search Engine Optimization 5-8 minutes

Laravel 13 Technical SEO: Meta Tags, Canonicals, Sitemaps, and Structured Data

Diego Cortés
Diego Cortés
Full Stack Developer & SEO Specialist
Share:
Laravel 13 Technical SEO: Meta Tags, Canonicals, Sitemaps, and Structured Data

On WordPress, SEO ships as plugins; in Laravel 13 you build it yourself, and that is the advantage. Laravel 13 technical SEO covers meta tags, canonicals, sitemaps, robots.txt, and structured data from Blade and packages like spatie, with total control over every tag Google sees.

Why Technical SEO in Laravel Is Different

WordPress Solves SEO with Plugins; Laravel Requires Code

A plugin like Yoast or RankMath writes the meta tags, generates the sitemap, and handles redirects on WordPress. In Laravel there is no such shortcut: every piece is implemented at the code level. What looks like a disadvantage is actually the biggest benefit: zero third-party plugins slowing down the application, tags exactly as you need them, and a codebase you fully understand. The typical failure points found in Laravel app audits are precisely what code solves: dynamic route bindings without canonicals, missing canonical definitions, and unoptimized queries that drag visibility down.

The Minimum Every Laravel App Needs to Get Indexed

For Google to understand your application you need five pieces: a unique title and description per page, a canonical tag pointing to the original URL, an XML sitemap listing the indexable content, a robots.txt with the crawl rules, and structured data when the content deserves it. On a bilingual blog like blenderdeluxe, hreflang joins the list, so the ES and EN versions do not compete with each other.

Meta Tags and Open Graph from Blade

The Layout as the Foundation: Title, Description, and Canonical per Page

Your application layout, usually a Blade component like x-app-layout, is the natural home for the meta block. Declare a stack and let every view inject its values:

<head>
    <title>@yield('title', config('app.name'))</title>
    @stack('meta')
</head>

In each view, @push fills in the values specific to that page:

@push('meta')
    <meta name="description" content="{{ $post->excerpt }}">
    <link rel="canonical" href="{{ url()->current() }}">
@endpush

This pattern leverages the Blade components you already use in Laravel 13 and keeps each page's SEO in a single place, without duplicating logic across views.

Open Graph and Twitter Cards for Social Sharing

When someone shares your URL on social networks, the link is rendered from the Open Graph tags. Add og:title, og:description, og:type, and og:image in the same stack, alongside the equivalent Twitter Cards. The social image deserves attention: it is the element that most decides whether a link gets clicked, and in Laravel 13 you can handle the asset build with Vite, as we covered in our Laravel 13 + Vite article.

Canonical: The URL That Rules

Why Duplicate Content Hurts Your Rankings

Google penalizes duplication: if the same page responds on several URLs, the search engine splits authority between them and none ranks well. In Laravel, duplication appears on its own: query parameters, pagination, sorting variants, or the same content reachable by slug and by id. The canonical tag tells Google which URL holds the original content.

Dynamic Canonicals with Slugs, Pagination, and Query Parameters

The solution is to generate the canonical dynamically on each page:

<link rel="canonical" href="{{ url()->current() }}">

For a post, the canonical URL is the slug one, without tracking parameters or language variants. On paginated listings, each result page declares its own canonical to itself, except the first one, which can point to the listing root. The golden rule: one page, one canonical, always pointing to the version you want to rank.

Dynamic XML Sitemaps with spatie/laravel-sitemap

Static Routes and Models in the Same Sitemap

The spatie/laravel-sitemap package generates sitemaps programmatically, combining static routes and models. On a blog, you want the main pages plus every published post:

use Spatie\Sitemap\Sitemap;

Sitemap::create()
    ->add('/')
    ->add('/about')
    ->add(Post::published()->get())
    ->writeToFile(public_path('sitemap.xml'));

The package iterates the model, uses its route, and respects the updated_at field for lastmod. Connect it to a scheduled task to regenerate the sitemap whenever content changes.

Sitemap Indexes, lastmod, and Caching the XML

When the site grows, use a sitemap index grouping several files: one for posts, one for categories, one for static pages. spatie/laravel-sitemap supports this format with SitemapIndex. lastmod should reflect the real last modification, and the XML is worth caching or generating on demand through a dedicated route, instead of rebuilding the whole sitemap on every request.

robots.txt and X-Robots-Tag with spatie/laravel-robots-middleware

Noindex for Admin, Drafts, and Staging Environments

spatie/laravel-robots-middleware decides per page whether it should be indexed, combining robots.txt, robots meta tags, and X-Robots-Tag headers. By default it allows indexing on every route; extend your own class and define your application rules:

class RobotsMiddleware extends RobotsMiddleware
{
    protected function shouldIndex(Request $request): bool
    {
        return $request->is('admin/*') || $request->is('drafts/*')
            ? false
            : parent::shouldIndex($request);
    }
}

This way the admin panel and drafts emit noindex automatically, and the resulting robots.txt mirrors the same rules. It is the difference between a staging environment Google ignores and one that gets indexed by accident.

Structured Data with JSON-LD

Article and BlogPosting for a Blog

Structured data enables rich results: stars, breadcrumbs, or the author in the SERP. For a blog, the BlogPosting type (a subtype of Article) communicates the title, date, author, and publisher to Google:

use Spatie\SchemaOrg\Schema;

$jsonLd = Schema::blogPosting()
    ->headline($post->title)
    ->datePublished($post->published_at->toIso8601String())
    ->author(Schema::person()->name('Diego Cortés'))
    ->publisher(Schema::organization()->name('Blender Deluxe'))
    ->toScript();

spatie/schema-org is a typed, fluent builder for the main Schema.org types and integrates with the same @stack flow as the meta tags.

FAQPage and BreadcrumbList with spatie/schema-org

If a post answers concrete questions, FAQPage JSON-LD can generate the question dropdown in Google. Breadcrumbs, with BreadcrumbList, improve how the site hierarchy is understood. Both are added just like BlogPosting, and restraint is wise: Google only shows rich results when the content genuinely justifies them.

Validating with Google's Rich Results Test

Before deploying, paste the URL into Google's Rich Results Test or validate the JSON-LD with the Schema.org validator. A syntax error in the JSON does not break the page, but it silently removes the rich result. Include this validation in your release checklist.

Hreflang: Multilingual SEO for ES/EN Blogs

Canonical per Language and Sitemap per Language

On a bilingual blog, every page must declare its language alternatives. In the post view you add the hreflang links:

<link rel="alternate" hreflang="es" href="{{ $post->urlEs }}">
<link rel="alternate" hreflang="en" href="{{ $post->urlEn }}">
<link rel="alternate" hreflang="x-default" href="{{ $post->urlEs }}">

Combined with a canonical per language and a sitemap that includes both versions, the search engine understands it is the same content in two languages. Without hreflang, the two versions compete for the same queries and both rank worse than either would alone.

x-default and Common Hreflang Mistakes

The x-default value points to the default version for users without a preferred language. The classic mistakes: declaring hreflang to a URL that redirects, forgetting the canonical per language, or declaring hreflang one way but not the other. The rule is simple: if page A points to B, B must point back to A, and both must be reachable without redirects.

301 Redirects and Clean URLs

Route::redirect and Slug Changes Without Losing Authority

When you change a slug or move a section, a 301 redirect transfers authority from the old URL to the new one. Laravel handles it in one line:

Route::redirect('/blog/old-slug', '/blog/new-slug', 301);

The same works for domain migrations or unifying www and non-www variants. Keep a redirect log so chains do not pile up, and avoid redirecting to the homepage: every 301 should land on the equivalent page, or you lose the context of the original query.

Technical SEO Release Checklist

Before shipping a Laravel 13 app, go through this list: unique title and description per page, dynamic canonical without parameters, regenerated and cached sitemap, correct robots.txt, noindex on admin and drafts, validated JSON-LD, bidirectional hreflang on multilingual sites, and 301 redirects for old URLs. With this covered, indexing stops being a mystery and becomes just another property of your code.

Conclusion

Laravel 13 technical SEO does not need plugins: it needs a well-organized layout, a couple of spatie packages, and the discipline to keep canonicals, sitemaps, and robots in order. The payoff is total control that no CMS gives you. Keep reading the blog for more Laravel 13 guides, from Blade components to queues and jobs in production.

Categories