Every tab in your app says “Laravel”, and every page shares the same meta description, or two of them. Here is how the Vue starter kit already builds titles, and the one head-key that keeps descriptions from piling up.
TL;DR
- Set
VITE_APP_NAMEin.envand restartnpm run dev:<Head title="Posts" />becomesPosts - My App. - Give the layout and the page the same
head-key="description"so only one description survives. - Keep
config('app.name')andVITE_APP_NAMEidentical so the first paint and the next click agree.
A Vue page renders inside the document body. It cannot write the <title> by putting a tag in the template and hoping the browser promotes it. Inertia’s <Head> does that write. The setup guide already uses <Head title="Posts" /> on the first list. This page is the app name and the description that list still lacks.
The stack is Laravel and Vue in 2026. Server rendering of the same tags is Inertia SSR.
The app name is already appended#
The Vue starter kit sets the callback in resources/js/app.ts:
const appName = import.meta.env.VITE_APP_NAME || "Laravel";
title: (title) => (title ? `${title} - ${appName}` : appName),<Head title="Posts" /> becomes Posts - My App once VITE_APP_NAME is My App. Leave the callback. An app that does not have it yet should add that title function inside the existing createInertiaApp call, next to the layout callback and progress. Do not start a second createInertiaApp.
APP_NAME="My App"
VITE_APP_NAME="${APP_NAME}"Gotcha: restart the dev server
VITE_APP_NAME belongs in .env. Vite only exposes names that start with VITE_, which the Vite guide already covers. Restart npm run dev after you change it. A running dev server keeps the previous value.
When a page sets no title, the callback keeps the app name. It does not render a leading hyphen.
One description per page#
A layout can ship a default description. A page replaces it. Both tags need the same head-key, or the document keeps both and the first one is the one a crawler may read.
<Head title="Posts">
<meta
head-key="description"
name="description"
content="Posts you have published."
/>
</Head>Inertia renders a single <title>. It stacks every other head tag unless head-key marks them as the same slot. Use description for that slot on the layout and on the page.
Count the descriptions
Open a page, then run this in the browser console. The answer should be 1.
document.querySelectorAll('meta[name="description"]').lengthThe first HTML response#
The first HTML response, before Vue runs, still comes from resources/views/app.blade.php. A title in that file is the fallback. <Head> replaces it on the visit. When SSR is off, the Blade component below is the fallback and is skipped once SSR is rendering the head itself:
<x-inertia::head>
<title>{{ config('app.name') }}</title>
<meta name="description" content="Posts you have published." />
</x-inertia::head>Heads up: two settings, one name
config('app.name') and VITE_APP_NAME are two settings. Set both to the same words, or the first paint and the next click disagree.
Recap#
- The starter kit’s
titlecallback appendsVITE_APP_NAME; keep it in your onecreateInertiaApp. - Restart Vite after changing
VITE_variables. - Use
head-key="description"on the layout and the page so the page wins. <x-inertia::head>in Blade is the fallback for the first response.
FAQ#
Why does my Inertia page title still say Laravel?
VITE_APP_NAME is not set, or the dev server was started before you set it. Add it to .env and restart npm run dev.
Why do I have two meta descriptions?
Inertia stacks head tags unless they share a head-key. Put head-key="description" on both the layout default and the page tag.
Can I put a title tag directly in a Vue template?
No. The page renders inside the body. Use Inertia’s <Head> component, which writes to the document head.
Next steps#
- Keep the layout between Inertia visits: the other callback in
createInertiaApp. - Server-render an Inertia app: titles in the first HTML.
- Laravel Vite and Vue: how
VITE_variables reach the browser. - Inertia docs: title and meta



