Laravel SEO: Blade or Inertia first, then titles, slugs, 301s, canonicals and a sitemap
How a Laravel 13 site gets its HTML to crawlers, and the six settings the framework leaves for you to write.
Published by AI Knows Us · Updated 21 October 2026
Laravel SEO comes down to one decision and a short list of settings. The decision is how your pages render: Blade and Livewire views are built on the server, so a search engine receives finished HTML, while the React, Vue and Svelte starter kits use Inertia, which needs server-side rendering switched on before crawlers see your text. The settings are yours to write: a unique title and meta description per page, slug URLs, 301 redirects for moved pages, a canonical tag that uses the right scheme behind a load balancer, a sitemap and cache headers. Everything below comes from the Laravel 13 documentation and the Inertia 3 documentation, read on 22 October 2026. Laravel 13 was released on 17 March 2026.
Blade or Inertia: the choice that decides most of it
Laravel itself does not stop a page from ranking. What matters is whether the first HTML response already holds the words you want found. Google's JavaScript SEO guide says crawling and parsing the HTML "works well" for server-side rendered pages, and adds that "server-side or pre-rendering is still a great idea" because it is faster for users and crawlers and "not all bots can run JavaScript".
| How the page is built | What a crawler gets first | What to do |
|---|---|---|
| Blade views, or the Livewire starter kit | Full HTML from the server | Add titles, descriptions and canonicals in the layout |
| Inertia (React, Vue or Svelte kit) with SSR off | A shell, with content filled in by JavaScript | Turn SSR on for any page you want indexed |
| Inertia with SSR on | Full HTML rendered by a Node process | Use the Head component and keep the SSR server running |
Laravel's starter kit docs say the React, Svelte and Vue kits are compatible with Inertia SSR, and you build the SSR bundle with npm run build:ssr. Inertia's own docs say its SSR server needs Node.js 22 or higher on your server. Laravel's Vite docs give php artisan inertia:start-ssr as the command to start it. If you plan a content-heavy marketing site, a Blade or Livewire front end gives you server HTML with no extra process to keep alive.
Titles and meta descriptions in a Blade layout
Put the head tags in your main layout once, and let each view fill them in. Blade's @yield and @section directives are the simplest way.
<!-- resources/views/layouts/app.blade.php -->
<head> <title>@yield('title', config('app.name'))</title> <meta name="description" content="@yield('description')"> <link rel="canonical" href="{{ url()->current() }}">
</head> <!-- resources/views/posts/show.blade.php -->
@extends('layouts.app')
@section('title', $post->title)
@section('description', $post->excerpt)
Write each title for the search it should win, and keep each description specific to its page. Blade escapes {{ }} output, so a quote mark in a post title will not break the tag.
Inertia pages: the Head component and head-key
Inertia's docs explain that an Inertia page renders inside the document body, so it cannot write to the head on its own. Inertia ships a <Head> component for React and Vue that sets the title and meta tags. The Svelte adapter does not include it, because Svelte has its own <svelte:head>.
One detail catches people: Inertia renders only one <title>, but other tags stack. If your layout sets a default description and the page sets another, you get two. Give both the same head-key so only one is rendered.
import { Head } from '@inertiajs/react' <Head title={post.title}> <meta head-key="description" name="description" content={post.excerpt} />
</Head>
Inertia also accepts a title callback in createInertiaApp, which is the place to add your site name to every title. If your Vue app runs on its own rather than inside Laravel, our guide to Vue and Nuxt SEO covers the same rendering choice from the Vue side.
Slug URLs with route model binding
Readable URLs such as /posts/laravel-seo-checklist are easy to build. Laravel can resolve a model by a column other than id directly in the route, and the Str::slug helper turns a title into a URL-friendly slug when you save the post.
use App\Models\Post; Route::get('/posts/{post:slug}', function (Post $post) { return view('posts.show', ['post' => $post]);
});
If every route for a model should use the slug, Laravel 13 lets you put the #[RouteKey('slug')] attribute on the model instead. When no record matches, Laravel returns a 404. If you would rather send visitors from an old slug somewhere useful, the route's missing method takes a closure that can redirect.
Redirects and status codes
This is the setting most often wrong on Laravel sites. The docs say Route::redirect returns a 302 by default. A 302 tells search engines the move is temporary. For a page that has moved for good, use a 301.
Route::permanentRedirect('/old-pricing', '/pricing'); // the same thing, with the status code passed in
Route::redirect('/old-pricing', '/pricing', 301);
Two more status code habits help:
- Keep 404s real.
Route::fallbackruns when nothing else matches. If you use it, make it return a 404 status, not a friendly page with a 200, or every mistyped URL becomes a thin indexable page. - Trailing slashes are handled on Apache. The default
public/.htaccessin the Laravel application skeleton redirects a trailing slash URL to the version without it, with a 301, unless the path is a real folder. On Nginx, add an equivalent rule yourself so each page has one address.
Canonical tags, and the load balancer problem
The url() helper builds URLs from the scheme and host of the current request. Behind a load balancer that ends TLS, the request reaching Laravel can arrive over plain HTTP, and the docs warn that your app may then fail to generate HTTPS links. That turns your canonical tag into an http:// address, which points search engines at the wrong version of every page.
The fix is the TrustProxies middleware, configured in bootstrap/app.php. You list the proxy addresses, or use '*' on a cloud load balancer whose addresses you do not know. Check the result by viewing the source of a live page and reading the canonical.
->withMiddleware(function (Middleware $middleware): void { $middleware->trustProxies(at: '*');
})
Watch query strings too. Laravel's paginator puts the page number in a page query string, and url()->current() leaves the query string out. That suits most filters and tracking tags. For paginated archives, give page 2 and onwards a canonical that includes the page number, so Google indexes the posts listed there rather than treating every page as page 1.
Sitemap and robots.txt
The Laravel skeleton ships a public/robots.txt that allows every crawler: User-agent: * followed by an empty Disallow:. Add a Sitemap: line pointing at your sitemap's full URL, and block only what you truly want kept out, such as admin paths.
For the sitemap itself, one package built for the job is spatie/laravel-sitemap. Its README says it can crawl your site to build the sitemap, take URLs you add by hand with a last-modified date, or do both. For a site whose pages live in the database, building it by hand from your models is usually cleaner than a crawl, because you control exactly which URLs appear.
use Spatie\Sitemap\Sitemap;
use Spatie\Sitemap\Tags\Url; $sitemap = Sitemap::create();
foreach (Post::published()->get() as $post) { $sitemap->add(Url::create(route('posts.show', $post)) ->setLastModificationDate($post->updated_at));
}
$sitemap->writeToFile(public_path('sitemap.xml'));
Run that from a scheduled command so the file stays current, then submit the sitemap URL in Search Console.
Speed and caching
Laravel includes a cache.headers middleware that sets Cache-Control for a group of routes. Add etag to the directives and Laravel sets an ETag from a hash of the response. Public pages that rarely change, such as legal pages and older articles, are good candidates. The Vite docs say a production build versions your assets, so CSS and JavaScript can be cached for a long time without stale files.
Check what a crawler receives
Fetch a page with curl and search the response for its heading and first paragraph. On a Blade site they will be there. On an Inertia site, if you see only a root element and a JSON blob, SSR is not running for that route. Then open the URL Inspection tool in Search Console and compare the rendered HTML with what you expect. Do this after every deploy that touches the SSR server or the proxy settings, because those are the two things that fail silently.
Questions about Laravel SEO
Is Laravel good for SEO?
Yes. Blade renders full HTML on the server, routes give you clean URLs, and you control every status code and header. The work is in the settings above, not in the framework.
Does Inertia hurt SEO in Laravel?
Only with SSR off. Without it, the first response is a shell and your text depends on the crawler running JavaScript. Turn SSR on with npm run build:ssr and keep the Node process running.
Why does my Laravel redirect show as 302?
Route::redirect returns 302 by default. Use Route::permanentRedirect, or pass 301 as the third argument, for pages that moved for good.
Why does my canonical tag say http instead of https?
Your app sits behind a proxy that ends TLS and Laravel does not trust it yet. Configure trustProxies in bootstrap/app.php.
How do I add a sitemap to Laravel?
Use a package such as spatie/laravel-sitemap, or write a route that returns XML built from your models. Rebuild it on a schedule and list it in robots.txt.
A rendered page is also a quotable page
Server HTML, clean slugs and honest status codes help more than Google. AI assistants that fetch a page while answering a question read the same first response, and a Laravel site that serves full text is easy for them to quote. Whether an assistant actually recommends your product when a buyer asks for one is a separate question that no rank tracker answers. Measuring that, engine by engine, is the job of a newer kind of tool, and AI Knows Us is built for it.