0% прочитано

The meta tags were in the browser but not in the HTML: why I needed Inertia SSR

I thought the SEO setup was already complete: the title, description, and canonical URL were all visible in the browser. But Google Search Console showed a different picture. That is how I realized I needed Inertia SSR.

25 липня 2026 р. 5 хв читанняInertia.js

How I discovered the problem

In the browser, the title, description, and canonical URL looked correct. But these SEO tags were missing from the initial HTML returned by the server and appeared only after JavaScript was executed.

I noticed this while checking the homepage in Google Search Console. The report did not show the canonical URL I had declared for the page.

At first, this seemed strange because all the metadata was present in DevTools. My SeoHead component received the data from Laravel through Inertia props and correctly added the tags to the head.

bash
curl -s https://icanup.com.ua/ \  | sed -n '/<head>/,/<\/head>/p'

The curl check showed a different picture. The server response contained only the basic application title, while the description and canonical URL were missing.

DevTools was showing the updated DOM after Vue had started, while curl showed the initial HTML returned before JavaScript was executed.

What was actually happening

До

<!-- Initial HTML -->

<title inertia>Icanup</title>

<!-- description missing -->

<!-- canonical missing -->

Після

<!-- DOM after Vue started -->

<title>Blog | IcanUp</title>

<meta

name="description"

content="Practical articles about PHP, Laravel, and Vue.js."

>

<link

rel="canonical"

href="https://icanup.com.ua"

>

The SEO data already existed. Laravel generated it correctly, Inertia passed it through the props, and the Vue component added the tags to the document.

The problem was the moment when this happened. The metadata appeared only on the client and was not present in the first server response.

Why I needed Inertia SSR

I did not want critical SEO metadata to depend entirely on JavaScript execution. I wanted the title, description, canonical URL, robots directive, Open Graph tags, and structured data to be present in the initial HTML. That is why I decided to add Inertia SSR.

What needs to be configured

To add SSR to my Inertia application, I need to:

  • create a separate server-side rendering entry point;

  • add an SSR build to Vite;

  • check that the Vue components are compatible with the server environment;

  • run Inertia SSR as a separate process;

  • manage the process with Supervisor;

  • update the deployment process to rebuild and restart SSR;

  • verify the SEO tags in the initial HTML.

How I set up Inertia SSR

First, I created a separate entry point for server-side rendering - resources/js/ssr.js.

In it, Inertia uses the same Vue pages and the same way of finding them as the client-side part of the application. The difference is that for the first request, the components are rendered via renderToString on the server.

javascript
import { createInertiaApp } from '@inertiajs/vue3'import createServer from '@inertiajs/vue3/server'import { renderToString } from '@vue/server-renderer' createServer(page =>    createInertiaApp({        page,        render: renderToString,        title: title => `${title} | IcanUp`,        resolve: name => resolvePageComponent(            `./Pages/${name}.vue`,            import.meta.glob('./Pages/**/*.vue'),        ),        setup: ({ App, props, plugin }) =>            createSSRApp({                render: () => h(App, props),            }).use(plugin),    }),)

After that, I added a separate SSR build in Vite. Now, during deployment, not only the client JavaScript is built, but also the server bundle.

The SSR itself runs as a separate Node.js process. I added it to Supervisor separately for dev and production so that the process would automatically start, restart after an error, and recover after a server reboot.

Deployment also had to be supplemented: after a new build, the SSR process must be restarted, otherwise it will continue to use the previous version of the bundle.

What had to be fixed during setup

The first run of SSR immediately showed where components depended on the browser environment.

In the client application, you can directly access window, document or localStorage. But during server rendering, these objects are not there.

After the edits, the same components were able to work both during SSR and after hydration in the browser.

I later ran into the same browser-vs-SSR split with routing: Ziggy worked correctly in the browser while SSR could receive a different routing configuration. I described that case separately in Making Ziggy Work Consistently in Blade, Vue and Inertia SSR.

Another problem appeared while checking the result: the original HTML had two title tags. One remained static in the Blade template, and the second was added by Inertia SSR. I removed the static title and left the head formation through Inertia.

How I will verify the result

For me, seeing the SSR process running in Supervisor is not enough. I also need to verify the actual server response.

bash
curl -s https://icanup.com.ua/ \  | sed -n '/<head>/,/<\/head>/p'

After setting up this command already showed the correct title, description, canonical, robots, Open Graph, Twitter and JSON-LD without launching the browser. It is also important that SSR rendered not only the head. The initial HTML already contained the page content, and Vue only connected the interactivity through hydration after loading.

After re-checking, Google Search Console crawled the updated page. The page remained available for indexing, and the current SEO metadata was already present in the initial server response.

Takeaway

At first, it seemed to me that SEO was already working, because in DevTools all meta tags were in place.

But DevTools showed the DOM after JavaScript execution, and not the initial server response. It was the verification via curl and Google Search Console that showed the difference.

In my case, Inertia SSR was not just an additional optimization. It covered a specific need: title, description, canonical and other SEO data are now returned along with the initial HTML.

At the same time, I did not have to abandon Vue or rewrite the pages on Blade. I left the Laravel, Inertia and Vue stack, but added server-side rendering for the first page load.