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

Server driven meta tags #240

wants to merge 28 commits into from

Conversation

bknoles
Copy link
Collaborator

@bknoles bknoles commented Jul 7, 2025

This is a feature I've wanted to add for a long time now. We built an ad hoc version of it ~3 years ago and have been using it in prod since then.

The biggest benefit to this implementation: per-page link previews in a React/Vue/Svelte app without JS SSR required.

The page I added to the docs is comprehensive, but as a quick preview, by adding a single line to the Rails layout and a copying a small component client-side, you can now do:

class SomeController < ApplicationController
  def show
    @the_thing = Thing.find(params[:id])

    render inertia 'SomePage', props: { thing: @the_thing }, meta: [
      { name: "description", content: @the_thing.description }
    ]
end

Inertia Rails will render a meta tag in the server rendered HTML that Inertia.js will pickup and "take over" rendering during Inertia visits.

There's also an inertia_meta controller instance method, that lets you share tags across actions:

class SomeController < ApplicationController
  before_action :set_default_meta_tags

  def index
     ...
  end

  def show
    ...
  end

  private

  def set_default_meta_tags
    inertia_meta.add([
      { title: "A common title" },
      { name: "description", content: "The fallback description" }
   ])
end

Inertia Rails intelligently deduplicates tags, so you can easily set and override defaults. Plus it uses the same head_key as Inertia.js if you want/need more control.

Test checklist/Todos

  • Works in a real React app
  • Works in a real Vue app
  • Works in a real Svelte app
  • Works with SSR (even though the purpose of this is to avoid SSR, i wanted to be sure it wasn't incompatible)
  • QA in our production web app (in process)

bknoles added 19 commits July 7, 2025 14:03
…class methods. Instance methods remove complexity and enable better control of the order of operations.
… page object so we don't depend on changes to Inertia.js
:::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

> 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 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.


## 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

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 😅

Comment on lines 145 to 147
document.head
.querySelectorAll('[inertia]')
.forEach(el => el.remove());
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 actually a lot simpler implementation than the React/Vue adapters have. Since you don't have to worry about built in <Head /> components defining their own meta tags you can safely clear all the server rendered tags and re-render them from scratch.

@@ -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.

@@ -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.

Digest::MD5.hexdigest(signature)[0, 8]
end

def generate_meta_head_key
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 added this after testing it in a real app based on feedback from our team. It seemed unncessary to have to add head_key: "description" everywhere when you know you want to dedup name: "description"` 100% of the time.

We dedup on the the values in :name, :property and :http_equiv automatically.

def handle_script_content(content)
case content
when String
@raw ? content.html_safe : ERB::Util.html_escape(content)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There's an (undocumented) escape hatch in case you want to render eval'able <script> tags. It's undocumented because I can't think of a good reason why you'd actually want to render a script tag like this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The html_safe and dangerouslySet make me a little nervous. At first glance I can't think of how someone could abuse those though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yea I dug back into my thinking and am going to make a couple changes. I don't think we need eval'able scripts after reconsidering it (I was originally thinking about analytics tracking scripts, but that doesn't really belong in this feature). And I need to escape the content on Inertia requests... the browser normally ignores script content created by dangerouslySetInnerHTML, but Inertia.js's <Head> renderer overrides it and make it eval'able again.

@meta_tags << new_tag
end

def duplicate?(existing_tag, new_tag)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Most of the duplication logic is in the head_key generation. Here we just check that, with a special second case for the title tag.

Copy link

cloudflare-workers-and-pages bot commented Jul 7, 2025

Deploying inertia-rails with  Cloudflare Pages  Cloudflare Pages

Latest commit: d91ad71
Status: ✅  Deploy successful!
Preview URL: https://8413f0fb.inertia-rails.pages.dev
Branch Preview URL: https://server-driven-meta-tags.inertia-rails.pages.dev

View logs

@bknoles bknoles requested a review from Copilot July 7, 2025 21:01
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds a server-side meta tag feature to Inertia Rails, allowing pages to define and render meta tags without requiring JS SSR.

  • Introduces MetaTag and MetaTagBuilder for tag creation, deduplication, and head-key generation
  • Updates the Renderer and adds a inertia_meta_tags helper to merge meta into Inertia props and render in the layout
  • Provides new specs, dummy controller routes, and docs/generator updates for the server-driven meta tags workflow

Reviewed Changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
lib/inertia_rails/meta_tag.rb Defines MetaTag behavior (JSON, HTML rendering, key gen)
lib/inertia_rails/meta_tag_builder.rb Implements MetaTagBuilder to add/remove/clear meta tags
lib/inertia_rails/renderer.rb Extends renderer to accept meta: and merge into page props
lib/inertia_rails/helper.rb Adds inertia_meta_tags helper for rendering tags in <head>
lib/inertia_rails/controller.rb Adds inertia_meta method to controllers
lib/inertia_rails.rb Passes meta option through the top‐level inertia method
spec/inertia/meta_tag_spec.rb Unit tests for MetaTag#to_json and head-key generation
spec/inertia/meta_tag_rendering_spec.rb Request specs for meta tags in Inertia JSON responses
spec/inertia/helper_spec.rb Helper tests for inertia_meta_tags
spec/dummy/config/routes.rb Adds dummy routes for all meta tag scenarios
spec/dummy/app/controllers/inertia_meta_controller.rb Dummy controller to drive meta tag examples
spec/dummy/app/controllers/concerns/meta_taggable.rb Concern for setting shared default meta
lib/generators/inertia/install/install_generator.rb Tweaks to always inject <title inertia>
lib/inertia_rails/generators/helper.rb Renames guess_typescript to guess_typescript?
lib/inertia_rails/generators/controller_template_base.rb Updates default typescript option to use guess_typescript?
docs/guide/title-and-meta.md Notes the new Rails‐managed meta tag support
docs/cookbook/server-managed-meta-tags.md Full cookbook for server-driven meta tags
docs/.vitepress/config.mts Registers the cookbook in nav
Comments suppressed due to low confidence (2)

docs/cookbook/server-managed-meta-tags.md:5

  • [nitpick] Consider hyphenating 'server defined' to 'server-defined' for clarity and consistency.
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.

lib/inertia_rails/renderer.rb:95

  • [nitpick] Add unit tests for computed_props (including _inertia_meta) to verify that meta tags are merged correctly when provided or absent.
      deep_transform_props(merged_props).merge({

Copy link
Collaborator

@BrandonShar BrandonShar left a comment

Choose a reason for hiding this comment

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

This is awesome! About time you upstreamed it 😉

Docs look great at a glance, but I'm pretty tired right now so I want to give the implementation a second look when I have more brainpower to give it a proper review.

@brodienguyen
Copy link
Contributor

@bknoles Is it backward-compatible when we use the syntax that omitting the component name with props on render please?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants