Master SEO in Inertia.js: A Clear, Practical Guide

Seo on page

Inertia.js apps render on the client, which can make SEO tricky. This guide shows how to add proper SEO using a reusable component.

Why SEO Matters for Inertia.js Apps

Even though Inertia isn’t a full-blown SPA framework, it still renders pages dynamically and relies heavily on JavaScript. For marketing sites, blogs, landing pages, SaaS dashboards, or any content that needs ranking, proper SEO ensures:

  • Your content gets indexed correctly
  • Titles and descriptions reflect each page
  • Social media previews display cleanly
  • Google understands your structure and relevance

How Inertia.js Handles SEO

Inertia.js does not render HTML on the client side like classic SPAs. Instead:

  • Your backend (Laravel, Rails, etc.) still controls routes and HTML.
  • Pages load via JSON responses and dynamic component replacement.
  • Metadata must be manually handled—especially titles, descriptions, and social tags.

The solution: a reusable SEO component that sets meta tags for each page.

Building the SEO Component

Create a component that handles all meta tags.

// resources/js/components/seo.tsx
import { Head, usePage } from '@inertiajs/react';
import { SharedData } from '@/types';
 
interface SEOProps {
title?: string;
description?: string;
image?: string;
imageAlt?: string;
url?: string;
type?: 'website' | 'article';
publishedTime?: string;
modifiedTime?: string;
author?: string;
keywords?: string[];
noindex?: boolean;
nofollow?: boolean;
}
 
export default function SEO({ ...
title = 'Mountaga Diao Leye Diop | モンタガ',
description = 'Fullstack Web Developer crafting high-performance web applications with Laravel & React JS. Passionate about clean code, scalable solutions, and bringing ideas to life through technology.',
image,
imageAlt,
url,
type = 'website',
publishedTime,
modifiedTime,
author = 'Mountaga Diao Leye Diop',
keywords = [],
noindex = false,
nofollow = false,
}: SEOProps) {
const page = usePage<SharedData>();
const appUrl = page.props.appUrl ? String(page.props.appUrl) : '';
const siteUrl = appUrl || (typeof window !== 'undefined' ? String(window.location.origin) : 'http://mdld.test');
const fullUrl = url ? String(`${siteUrl}${url}`) : String(siteUrl);
const fullImage = image ? (image.startsWith('http') ? String(image) : String(`${siteUrl}${image}`)) : String(`${siteUrl}/favicon.png`);
 
const robotsContent = String([
noindex ? 'noindex' : 'index',
nofollow ? 'nofollow' : 'follow',
].join(', '));
 
const titleString = String(title);
const descriptionString = String(description);
const typeString = String(type);
const authorString = String(author);
const imageAltString = imageAlt ? String(imageAlt) : '';
 
return (
<Head>
<title>{titleString}</title>
<meta head-key="description" name="description" content={descriptionString} />
{keywords.length > 0 && (
<meta head-key="keywords" name="keywords" content={String(keywords.join(', '))} />
)}
<meta head-key="author" name="author" content={authorString} />
<meta head-key="robots" name="robots" content={robotsContent} />
 
<meta head-key="og:type" property="og:type" content={typeString} />
<meta head-key="og:url" property="og:url" content={fullUrl} />
<meta head-key="og:title" property="og:title" content={titleString} />
<meta head-key="og:description" property="og:description" content={descriptionString} />
<meta head-key="og:image" property="og:image" content={fullImage} />
{imageAltString && (
<meta head-key="og:image:alt" property="og:image:alt" content={imageAltString} />
)}
<meta head-key="og:site_name" property="og:site_name" content="Mountaga Diao Leye Diop | モンタガ" />
<meta head-key="og:locale" property="og:locale" content="en_US" />
 
{type === 'article' && publishedTime && (
<meta head-key="article:published_time" property="article:published_time" content={String(publishedTime)} />
)}
{type === 'article' && modifiedTime && (
<meta head-key="article:modified_time" property="article:modified_time" content={String(modifiedTime)} />
)}
{type === 'article' && author && (
<meta head-key="article:author" property="article:author" content={authorString} />
)}
 
<meta head-key="twitter:card" name="twitter:card" content="summary_large_image" />
<meta head-key="twitter:url" name="twitter:url" content={fullUrl} />
<meta head-key="twitter:title" name="twitter:title" content={titleString} />
<meta head-key="twitter:description" name="twitter:description" content={descriptionString} />
<meta head-key="twitter:image" name="twitter:image" content={fullImage} />
{imageAltString && (
<meta head-key="twitter:image:alt" name="twitter:image:alt" content={imageAltString} />
)}
<meta head-key="twitter:creator" name="twitter:creator" content="@MDLD_" />
 
<link head-key="canonical" rel="canonical" href={fullUrl} />
</Head>
);
}

What This Component Does

  1. Accepts props with defaults so you only pass what changes.
  2. Builds full URLs for SSR and client-side.
  3. Handles absolute and relative image paths.
  4. Uses head-key so Inertia updates tags on navigation.
  5. Supports basic SEO, Open Graph, Twitter, and article tags.

Understanding the Code

URL Handling

const siteUrl = page.props.appUrl || window.location.origin;
const fullUrl = url ? `${siteUrl}${url}` : siteUrl;

In SSR, use appUrl from props. On the client, fall back to window.location.origin. Always build full URLs for social sharing.

Image Handling

const fullImage = image
? (image.startsWith('http') ? image : `${siteUrl}${image}`)
: `${siteUrl}/favicon.png`;

Social platforms need absolute URLs. If the image is already absolute, use it; otherwise prepend the site URL. If no image is provided, use the favicon.

The head-key Attribute

<meta head-key="description" name="description" content={description} />

Inertia uses head-key to identify and update meta tags on navigation. Without it, tags may not update when navigating between pages.

Default Values

title = 'Your Default Title',
description = 'Fullstack Web Developer...'

Defaults values let you skip props on simple pages. Override them when needed.

Using the Component

Example 1: Home Page

import SEO from '@/components/seo';
 
export default function Home() {
return (
<MainLayout>
<SEO
title="Home"
description="Welcome to my website"
url="/"
/>
{/* Your page content */}
</MainLayout>
);
}

Explanation: Only pass what differs from defaults. The component handles the rest.

Example 2: Blog Post

export default function PostShow({ post }) {
return (
<MainLayout>
<SEO
title={post.title}
description={post.excerpt}
image={post.image ? `/storage/${post.image}` : ''}
imageAlt={post.image_alt || post.title}
url={`/posts/${post.slug}`}
type="article"
publishedTime={post.published_at}
/>
{/* Post content */}
</MainLayout>
);
}

Explanation:

  • type="article" tells platforms this is a blog post.
  • publishedTime helps with article-specific features.
  • Image and alt text improve social previews.

Example 3: List Page

export default function PostsIndex() {
return (
<MainLayout>
<SEO
title="Posts"
description="All blog posts"
url="/posts"
keywords={['blog', 'posts', 'articles']}
/>
{/* Posts list */}
</MainLayout>
);
}

Explanation: Keywords are optional. Use them for list/category pages if helpful.

Understanding Meta Tags

Basic SEO Tags

  • <title>: Page title in browser tabs and search results.
  • <meta name="description">: Snippet in search results (keep it under 160 characters).
  • <meta name="keywords">: Optional; use sparingly.

Open Graph Tags (Facebook, LinkedIn)

  • og:type: website or article.
  • og:url: Full URL of the page.
  • og:title: Title for social shares.
  • og:description: Description for social shares.
  • og:image: Image for social shares (1200x630px recommended).

Open Graph tags control how links appear when shared on Facebook, LinkedIn, and similar platforms.

Twitter Card Tags

  • twitter:card: summary_large_image shows a large image.
  • twitter:title, twitter:description, twitter:image: Content for Twitter.

Twitter uses its own tags. If Open Graph tags are present, Twitter may use them, but explicit Twitter tags give more control.

Article-Specific Tags

article:published_time: When the article was published (ISO format like 2024-01-15T10:00:00Z).

Why: Helps search engines and platforms understand the content better.

Canonical URL

<link rel="canonical" href={fullUrl} />

Tells search engines which URL is the primary version, avoiding duplicate content issues.

Real Examples from my Blog

<SEO
title={post.title}
description={post.excerpt || post.title}
image={postImage || ''}
imageAlt={post.image_alt || post.title}
url={postUrl}
type="article"
publishedTime={post.published_at}
modifiedTime={post.published_at}
/>

type="article" tells platforms this is a blog post, enabling article-specific features and the fallback description is there if there’s no excerpt to use the title as the description.

Conclusion: That’s It!

Inertia.js is powerful, flexible, and modern—but SEO isn’t automatic. Thankfully, you don’t need heavy SSR engines or complex setups. A few simple steps are enough to make your app discoverable:

  • Manage titles and metadata
  • Use clean URLs
  • Add structured data when relevant
  • Optimize performance
  • Ensure your pages can be crawled

Follow these guidelines, and your Inertia.js app will not only feel fast—it will be visible, discoverable, and ranking.