Skip to content

Server driven meta tags #240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e719c95
Add server managed meta tags to the page object
bknoles Apr 24, 2025
58f417f
Add shared meta tags, similar to shared props
bknoles Apr 25, 2025
22ce34b
Naming things is important
bknoles Apr 25, 2025
a8b0b37
Local assigns doesn't work here so let's use a controller instance va…
bknoles Apr 26, 2025
25cb5bb
head-key is awkward to consume in JS and doesn't offer any benefits s…
bknoles Apr 26, 2025
9ef9056
React and Vue both know how to deal with camelCase so let's use that …
bknoles May 1, 2025
0d752d7
Instead of overloading the content key, use an inner_content for what…
bknoles May 2, 2025
9e6c2b4
Allow a shortened version of the title meta tag
bknoles May 2, 2025
d72efad
Only compute a single title tag
bknoles May 2, 2025
4919b72
Refactor inertia meta to use a controller instance method instead of …
bknoles Jun 20, 2025
45d3604
Use props._inertia_meta instead of adding a meta key to the top level…
bknoles Jun 20, 2025
4fb200b
Extra code cleanup from the redesign to use instance variables for me…
bknoles Jun 20, 2025
19735cd
Intelligently dedup meta tags without requiring head_key
bknoles Jun 24, 2025
8af1ebc
Opt-out functionality for automatic meta tag de-duping
bknoles Jun 24, 2025
3d0f9f9
Code cleanup
bknoles Jun 24, 2025
016112f
De-duplication must come from the head key to work client side
bknoles Jun 25, 2025
f218388
Allow clearing of meta tags
bknoles Jul 1, 2025
8127bfe
Add documentation for server managed meta tags
bknoles Jul 7, 2025
316b7a9
Add inertia tag to title with Svelte since server managed tags use it
bknoles Jul 7, 2025
fb4eb47
Prettier fixes for docs
bknoles Jul 7, 2025
591cbe9
Small docs clarification
bknoles Jul 7, 2025
87c1dc3
Appease the rubocop
bknoles Jul 7, 2025
ab16708
Fix method signature to match what earlier Rails versions expect
bknoles Jul 7, 2025
3cb496f
Fix Rubocop error
bknoles Jul 7, 2025
987b3db
Wrap Vue meta tag component in a function
bknoles Jul 10, 2025
747c48e
Rework script tag safety. JSON-LD gets through, everything else gets …
bknoles Jul 10, 2025
4e711d7
Docs updates for JSON LD
bknoles Jul 10, 2025
d91ad71
Update docs/guide/title-and-meta.md
bknoles Jul 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ export default defineConfig({
{ text: 'Inertia Modal', link: '/cookbook/inertia-modal' },
],
},
{
text: 'Inertia-Rails-Only Features',
items: [
{
text: 'Server Managed Meta Tags',
link: '/cookbook/server-managed-meta-tags',
},
],
},
{
text: 'Troubleshooting',
items: [
Expand Down
360 changes: 360 additions & 0 deletions docs/cookbook/server-managed-meta-tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
# Server Managed Meta Tags

Inertia Rails can manage a page's meta tags on the server instead of on the frontend. This means that link previews (such as on Facebook, LinkedIn, etc.) will include correct meta _without server-side rendering_.

Inertia Rails renders server defined meta tags into both the server rendered HTML and the client-side Inertia page props. Because the tags share unique `head-key` attributes, the client will "take over" the meta tags after the initial page load.

## Setup

### Server Side

Simply add the `inertia_meta_tags` helper to your layout. This will render the meta tags in the `<head>` section of your HTML.

```erb
<!-- app/views/layouts/application.html.erb (or your custom layout) -->

<!DOCTYPE html>
<html>
<head>
...
<%= inertia_meta_tags %> <!-- Add this inside your <head> tag -->
</head>
</html>
```

> [!NOTE]
> If you have a `<title>` tag in your Rails layout, make sure it has the `inertia` attribute on it so Inertia knows it should deduplicate it. The Inertia Rails install generator does this for you automatically.

### Client Side
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about releasing this in a separate package or trying to merge this into Inertia.js core, but decided the path of least resistance was just to add instructions to copy paste the (tiny) component required to make this work.


Copy the following code into your application. It should be rendered **once** in your application, such as in a [layout component
](/guide/pages#creating-layouts).

:::tabs key:frameworks
== Vue

```vue
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested all of these components in shiny new Inertia Rails apps. That said, I've never shipped production Vue/Svelte code. Extra eyes would be appreciated

<script>
import { Head } from '@inertiajs/vue3'
import { usePage } from '@inertiajs/vue3'
import { h } from 'vue'

export default {
name: 'MetaTags',
setup() {
const page = usePage()

return () => {
const metaTags = page.props._inertia_meta || []

return h(Head, {}, () =>
metaTags.map((meta) => {
const { tagName, innerContent, headKey, httpEquiv, ...attrs } = meta

const attributes = {
key: headKey,
'head-key': headKey,
...attrs,
}

if (httpEquiv) {
attributes['http-equiv'] = httpEquiv
}

let content = null
if (innerContent != null) {
content =
typeof innerContent === 'string'
? innerContent
: JSON.stringify(innerContent)
}

return h(tagName, attributes, content)
}),
)
}
},
}
</script>
```

== React

```jsx
import React from 'react'
import { Head, usePage } from '@inertiajs/react'

const MetaTags = () => {
const { _inertia_meta: meta } = usePage().props
return (
<Head>
{meta.map((meta) => {
const { tagName, innerContent, headKey, httpEquiv, ...attrs } = meta

let stringifiedInnerContent
if (innerContent != null) {
stringifiedInnerContent =
typeof innerContent === 'string'
? innerContent
: JSON.stringify(innerContent)
}

return React.createElement(tagName, {
key: headKey,
'head-key': headKey,
...(httpEquiv ? { 'http-equiv': httpEquiv } : {}),
...attrs,
...(stringifiedInnerContent
? { dangerouslySetInnerHTML: { __html: stringifiedInnerContent } }
: {}),
})
})}
</Head>
)
}

export default MetaTags
```

== Svelte 4|Svelte 5

```svelte
<!-- MetaTags.svelte -->
<script>
import { onMount } from 'svelte'
import { page } from '@inertiajs/svelte'

$: metaTags = ($page.props._inertia_meta ?? []).map(
({ tagName, headKey, innerContent, httpEquiv, ...attrs }) => ({
tagName,
headKey,
innerContent,
attrs: httpEquiv ? { ...attrs, 'http-equiv': httpEquiv } : attrs,
}),
)

// Svelte throws warnings if we render void elements like meta with content
$: voidTags = metaTags.filter((tag) => tag.innerContent == null)
$: contentTags = metaTags.filter((tag) => tag.innerContent != null)

let ready = false

onMount(() => {
// Clean up server-rendered tags
document.head.querySelectorAll('[inertia]').forEach((el) => el.remove())

ready = true
})
</script>

<svelte:head>
{#if ready}
<!-- Void elements (no content) -->
{#each voidTags as tag (tag.headKey)}
<svelte:element this={tag.tagName} inertia={tag.headKey} {...tag.attrs} />
{/each}

<!-- Elements with content -->
{#each contentTags as tag (tag.headKey)}
<svelte:element this={tag.tagName} inertia={tag.headKey} {...tag.attrs}>
{@html typeof tag.innerContent === 'string'
? tag.innerContent
: JSON.stringify(tag.innerContent)}
</svelte:element>
{/each}
{/if}
</svelte:head>
```

:::

## Rendering Meta Tags

Tags are defined as plain hashes and conform to the following structure:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at the Rails meta_tags gem and Remix.run's meta objects for inspiration. I decided to minimize "magic". The data structure just describes an HTML, with a few exceptions:

  • tag_name will default to :meta if it isn't defined
  • inner_content is used for non-void tags
  • title has shortcut syntax


```ruby
# All fields are optional.
{
# Defaults to "meta" if not provided
tag_name: "meta",

# Used for <meta http-equiv="...">
http_equiv: "Content-Security-Policy",

# Used to deduplicate tags. InertiaRails will auto-generate one if not provided
head_key: "csp-header",

# Used with <script>, <title>, etc.
inner_content: "Some content",

# Any additional attributes will be passed directly to the tag.
# For example: name: "description", content: "Page description"
name: "description",
content: "A description of the page"
}
```

The `<title>` tag has shortcut syntax:

```ruby
{ title: "The page title" }
```

### In the renderer

Add meta tags to an action by passing an array of hashes to the `meta:` option in the `render` method:

```ruby
class EventsController < ApplicationController
def show
event = Event.find(params[:id])

render inertia: 'Event/Show', props: { event: event.as_json }, meta: [
{ title: "Check out the #{event.name} event!" },
{ name: 'description', content: event.description },
{ tag_name: 'script', type: 'application/ld+json', inner_content: { '@context': 'https://schema.org', '@type': 'Event', name: 'My Event' } }
]
end
end
```

### Shared Meta Tags

Often, you will want to define default meta tags that are shared across certain pages and which you can override within a specific controller or action. Inertia Rails has an `inertia_meta` controller instance method which references a store of meta tag data.

You can call it anywhere in a controller to manage common meta tags, such as in `before_action` callbacks or directly in an action.

```ruby
class EventsController < ApplicationController
before_action :set_meta_tags

def show
render inertia: 'Event/Show', props: { event: Event.find(params[:id]) }
end

private

def set_meta_tags
inertia_meta.add([
{ title: 'Look at this event!' }
])
end
end
```

#### The `inertia_meta` API
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically the API I wish we had built for inertia_share. If you look at the commit history, I actually built out an entire implementation with inertia_meta as a controller class method, before remembering how much I regret doing that for inertia_share 😅


The `inertia_meta` method provides a simple API to manage your meta tags. You can add, remove, or clear tags as needed. The `inertia_meta.remove` method accepts either a `head_key` string or a block to filter tags.

```ruby
# Add a single tag
inertia_meta.add({ title: 'Some Page title' })

# Add multiple tags at once
inertia_meta.add([
{ tag_name: 'meta', name: 'og:description', content: 'A description of the page' },
{ tag_name: 'meta', name: 'twitter:title', content: 'A title for Twitter' },
{ tag_name: 'title', inner_content: 'A title for the page', head_key: 'my_custom_head_key' },
{ tag_name: 'script', type: 'application/ld+json', inner_content: { '@context': 'https://schema.org', '@type': 'Event', name: 'My Event' } }
])

# Remove a specific tag by head_key
inertia_meta.remove("my_custom_head_key")

# Remove tags by a condition
inertia_meta.remove do |tag|
tag[:tag_name] == 'script' && tag[:type] == 'application/ld+json'
end

# Remove all tags
inertia_meta.clear
```

#### JSON-LD and Script Tags

Inertia Rails supports defining `<script>` tags with `type="application/ld+json"` for structured data. All other script tags will be marked as `type="text/plain"` to prevent them from executing on the client side. Executable scripts should be added either in the Rails layout or using standard techniques in your frontend framework.

```ruby
inertia_meta.add({
tag_name: "script",
type: "application/ld+json",
inner_content: {
"@context": "https://schema.org",
"@type": "Event",
name: "My Event",
startDate: "2023-10-01T10:00:00Z",
location: {
"@type": "Place",
name: "Event Venue",
address: "123 Main St, City, Country"
}
}
})
```

## Deduplication

> [!NOTE]
> The Svelte adapter does not have a `<Head />` component. Inertia Rails will deduplicate meta tags _on the server_, and the Svelte component above will render them deduplicated accordingly.

### Automatic Head Keys

Inertia Rails relies on the `head-key` attribute and the `<Head />` components that the Inertia.js core uses to [manage meta tags](/guide/title-and-meta) and deduplicate them. Inertia.js core expects us to manage `head-key` attributes and deduplication manually, but Inertia Rails will generate them automatically for you.

- `<meta>` tags will use the `name`,`property`, or `http_equiv` attributes to generate a head key. This enables automatic deduplication of common meta tags like `description`, `og:title`, and `twitter:card`.
- All other tags will deterministically generate a `head-key` based on the tag's attributes.

#### Allowing Duplicates

Sometimes, it is valid HTML to have multiple meta tags with the same name or property. If you want to allow duplicates, you can set the `allow_duplicates` option to `true` when defining the tag.

```ruby
class StoriesController < ApplicationController
before_action do
inertia_meta.add({ name: 'article:author', content: 'Tony Gilroy' })
end


# Renders a single article:author meta tag
def single_author
render inertia: 'Stories/Show'
end


# Renders multiple article:author meta tags
def multiple_authors
render inertia: 'Stories/Show', meta: [
{ name: 'article:author', content: 'Dan Gilroy', allow_duplicates: true },
]
end
end
```

### Manual Head Keys

Automatic head keys should cover the majority of use cases, but you can set `head_key` manually if you need to control the deduplication behavior more precisely. For example, you may want to do this if you know you will remove a shared meta tag in a specific action.

```ruby
# In a concern or `before_action` callback
inertia_meta.add([
{
tag_name: 'meta',
name: 'description',
content: 'A description of the page',
head_key: 'my_custom_head_key'
},
])

# Later in a specific action
inertia_meta.remove('my_custom_head_key')
```

## Combining Meta Tag Methods

There are multiple ways to manage meta tags in Inertia Rails:

- Adding tags to a Rails layout such as `application.html.erb`.
- Using the `<Head />` component from Inertia.js (or the Svelte head element) in the frontend.
- Using the server driven meta tags feature described here.

Nothing prevents you from using these together, but for organizational purposes, we recommended using only one of the last two techniques.
3 changes: 3 additions & 0 deletions docs/guide/title-and-meta.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

Since Inertia powered JavaScript apps are rendered within the document `<body>`, they are unable to render markup to the document `<head>`, as it's outside of their scope. To help with this, Inertia ships with a `<Head>` component which can be used to set the page `<title>`, `<meta>` tags, and other `<head>` elements.

> [!NOTE]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this isn't a part of the Inertia.js core, I think it's better to talk about this in a cookbook instead of inside the main title/meta page.

> Since v3.10.0, Inertia Rails supports managing meta tags via Rails. This allows your meta tags to work with link preview services without setting up server-side rendering. Since this isn't a part of the Inertia.js core, it's documented in the [server driven meta tags cookbook](/cookbook/server-managed-meta-tags).

> [!NOTE]
> The `<Head>` component will only replace `<head>` elements that are not in your server-side layout.

Expand Down
2 changes: 1 addition & 1 deletion lib/generators/inertia/install/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def install_inertia
before: '<%= vite_client_tag %>'
end

gsub_file application_layout.to_s, /<title>/, '<title inertia>' unless svelte?
gsub_file application_layout.to_s, /<title>/, '<title inertia>'
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Svelte component from the doc above needs the inertia tag to properly dedup so I removed this.

else
say_error 'Could not find the application layout file. Please add the following tags manually:', :red
say_error '- <title>...</title>'
Expand Down
Loading