A single-page application still needs a well-managed document <head>. Page titles, descriptions, canonical URLs, and social previews remain important for every public route.
Inertia 3.x gives Laravel and Vue applications a clear way to manage these elements. The Head component from @inertiajs/vue3 works during client-side navigation and server-side rendering.
This matters most for content-heavy applications. Blogs, documentation sites, marketing pages, and product catalogs need unique metadata for every URL.
Laravel remains a productive PHP web framework. Inertia lets you pair it with Vue without maintaining a separate API for every page. The same application can render an article, expose a JSON endpoint, or help you build a REST API with PHP.
How Inertia’s Head component works
Inertia pages render inside the document <body>. They cannot write directly to the browser’s <head> without help.
The Head component provides that bridge:
<script setup>
import { Head } from '@inertiajs/vue3'
</script>
<template>
<Head title="About our company" />
<h1>About our company</h1>
</template>
This creates a document title:
<title>About our company</title>
You can also place regular head elements inside the component:
<Head>
<title>About our company</title>
<meta
name="description"
content="Learn how our company builds modern web applications."
/>
</Head>
Use the title prop for simple titles. Use the full component when you need descriptions, Open Graph tags, canonical links, or structured data.

Set global defaults in the root Blade template
Your root Blade template should provide safe fallback metadata. This covers the initial document when SSR is disabled.
A typical resources/views/app.blade.php file can look like this:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
@vite('resources/js/app.js')
<x-inertia::head>
<title>{{ config('app.name') }}</title>
<meta
data-inertia="description"
name="description"
content="Build modern web applications with {{ config('app.name') }}."
>
<meta
data-inertia="og:type"
property="og:type"
content="website"
>
<meta
data-inertia="og:site_name"
property="og:site_name"
content="{{ config('app.name') }}"
>
<link
data-inertia="canonical"
rel="canonical"
href="{{ config('app.url') }}"
>
</x-inertia::head>
</head>
<body>
@inertia
</body>
</html>
The <x-inertia::head> component provides fallback content when SSR is not active. The data-inertia values let Inertia adopt these elements during the first client-side navigation.
Avoid placing a permanent static <title> outside this component. It can conflict with the title managed by Vue.
These defaults are not a replacement for page-specific metadata. They are a safety net for the application shell.
Share application defaults with HandleInertiaRequests
The HandleInertiaRequests middleware is a useful place for values that your layouts and pages need.
It does not directly write tags into the Blade document. Instead, it shares data with every Inertia page.
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
public function share(Request $request): array
{
return [
...parent::share($request),
'app' => [
'name' => config('app.name'),
'url' => config('app.url'),
'description' => config('app.description'),
],
];
}
}
Your Vue application can use these shared values in a reusable head wrapper or layout.
<script setup>
import { Head, usePage } from '@inertiajs/vue3'
const page = usePage()
const app = page.props.app
</script>
<template>
<Head>
<meta
head-key="description"
name="description"
:content="app.description"
/>
<meta
head-key="og:site_name"
property="og:site_name"
:content="app.name"
/>
</Head>
<slot />
</template>
Keep the Blade fallback and shared Vue defaults aligned. Store common values in configuration so they do not drift.
For more middleware patterns, see Laravel’s middleware documentation.
How Head merges titles and metadata
Inertia supports multiple Head instances. A layout can define defaults, while a page supplies its own values.
A layout might include:
<Head>
<title>My application</title>
<meta
head-key="description"
name="description"
content="The default application description."
/>
<link
head-key="canonical"
rel="canonical"
href="https://example.com"
/>
</Head>
A page can override the description and canonical URL:
<Head title="Laravel SEO Guide">
<meta
head-key="description"
name="description"
content="Learn how to manage SEO metadata in a Laravel and Vue application."
/>
<link
head-key="canonical"
rel="canonical"
href="https://example.com/articles/laravel-seo"
/>
</Head>
Inertia renders one <title> element. The page title takes precedence over the layout title.
Other elements can appear more than once. Use head-key when a tag should have only one instance. Descriptions, canonical links, Open Graph properties, and structured data should usually have keys.
Without a key, repeated layout and page tags can stack:
<meta name="description" content="Default description">
<meta name="description" content="Page description">
That creates ambiguous metadata. Add a stable head-key instead.
Title arrays and titleAttribute
The Inertia 3.x Vue Head component accepts a title string. It does not provide special support for title arrays.
This is not an Inertia title API:
<Head :title="['Laravel', 'SEO', 'Guide']" />
Build the final string yourself:
<script setup>
import { Head } from '@inertiajs/vue3'
const titleSegments = ['Laravel', 'SEO', 'Guide']
const title = titleSegments.join(' | ')
</script>
<template>
<Head :title="title" />
</template>
Inertia also does not define a titleAttribute prop. If you encounter that name, it likely belongs to a custom wrapper or another package.
For a global title format, use the title callback in createInertiaApp:
createInertiaApp({
title: (title, page) => {
const appName = page.props.app?.name ?? 'My application'
return [title, appName]
.filter(Boolean)
.join(' | ')
},
// resolve, setup, and other options...
})
A page can now use a short title:
<Head title="Laravel SEO Guide" />
The browser receives:
<title>Laravel SEO Guide | My application</title>
A complete content page example
Suppose your Laravel application publishes technical articles.
The controller can provide the article and its SEO values:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Inertia\Inertia;
use Inertia\Response;
class PostController extends Controller
{
public function show(Post $post): Response
{
abort_unless($post->published_at?->isPast(), 404);
return Inertia::render('Posts/Show', [
'post' => [
'title' => $post->title,
'excerpt' => $post->excerpt,
'body' => $post->body,
'publishedAt' => $post->published_at?->toIso8601String(),
'updatedAt' => $post->updated_at?->toIso8601String(),
],
'seo' => [
'canonical' => route('posts.show', $post),
'image' => $post->social_image_url,
],
]);
}
}
The Vue page can then define all relevant tags:
<script setup>
import { computed } from 'vue'
import { Head } from '@inertiajs/vue3'
const props = defineProps({
post: {
type: Object,
required: true,
},
seo: {
type: Object,
required: true,
},
})
const structuredData = computed(() => JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
headline: props.post.title,
description: props.post.excerpt,
datePublished: props.post.publishedAt,
dateModified: props.post.updatedAt,
mainEntityOfPage: props.seo.canonical,
image: props.seo.image,
}))
</script>
<template>
<Head :title="post.title">
<meta
head-key="description"
name="description"
:content="post.excerpt"
/>
<link
head-key="canonical"
rel="canonical"
:href="seo.canonical"
/>
<meta
head-key="og:type"
property="og:type"
content="article"
/>
<meta
head-key="og:title"
property="og:title"
:content="post.title"
/>
<meta
head-key="og:description"
property="og:description"
:content="post.excerpt"
/>
<meta
head-key="og:url"
property="og:url"
:content="seo.canonical"
/>
<meta
head-key="og:image"
property="og:image"
:content="seo.image"
/>
<meta
head-key="twitter:card"
name="twitter:card"
content="summary_large_image"
/>
</Head>
<article>
<h1>{{ post.title }}</h1>
<p>{{ post.excerpt }}</p>
<div v-html="post.body" />
</article>
</template>
Use absolute URLs for canonical links and social images. Social crawlers cannot reliably resolve private, relative, or temporary paths.

SEO metadata and SSR
Without SSR, Inertia injects head elements in the browser after JavaScript loads. Users see the correct title, but the initial HTML source may contain only the fallback metadata.
SSR renders the Vue page on the server. Crawlers and link preview tools can then receive the page title, description, Open Graph tags, and JSON-LD in the initial response.
The Inertia SSR documentation covers the full setup. In production, the usual workflow includes:
npm run build
php artisan inertia:start-ssr
When SSR is enabled, the <x-inertia::head> fallback is skipped because the server-rendered Head component supplies the elements.
SSR does not generate metadata automatically. The controller must still provide the correct title, description, canonical URL, and image before rendering begins.
You should also ensure that metadata is available without browser-only APIs. Avoid reading window, document, or client-only state while computing SEO values.
For a new project, Laravel’s official Starter Kits include Vue 3 and Inertia 3 options. You can add SSR as your public content grows.

A practical metadata checklist
For every indexable page, verify the following:
- A unique
<title>exists. - The title callback adds the application name consistently.
- The meta description matches the page content.
- Every canonical link is absolute and points to the preferred URL.
- Open Graph tags use the same title and description.
-
og:imagepoints to a public, crawlable image. - Repeated tags use
head-key. - Structured data matches the visible content.
- SSR produces the expected tags in the initial HTML.
- Private dashboards and authenticated pages use appropriate
robotsdirectives.
The Head component keeps these concerns close to the page that owns them. Layouts define stable defaults. Controllers provide page data. Vue describes the final document head.
That separation makes SEO easier to review and harder to forget as your Laravel and Vue application grows.