inertia-rails-ssr

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Inertia Rails Server-Side Rendering (SSR)

Inertia Rails Server-Side Rendering (SSR)

Server-Side Rendering pre-renders JavaScript pages on the server, delivering fully rendered HTML to visitors. This improves SEO, enables faster initial page loads, and allows basic navigation even with JavaScript disabled.
服务端渲染(SSR)会在服务器上预渲染JavaScript页面,向访问者交付完全渲染好的HTML。这能提升SEO效果,加快初始页面加载速度,并且在禁用JavaScript的情况下仍能支持基础导航。

When to Use SSR

何时使用SSR

  • SEO-critical pages - Landing pages, blog posts, product pages
  • Social sharing - Pages that need proper Open Graph meta tags
  • Slow connections - Users see content before JavaScript loads
  • Accessibility - Basic functionality without JavaScript
  • SEO关键页面 - 落地页、博客文章、产品页
  • 社交分享 - 需要正确Open Graph元标签的页面
  • 网络连接缓慢 - 用户在JavaScript加载完成前就能看到内容
  • 可访问性 - 无需JavaScript即可使用基础功能

Requirements

前提条件

  • Node.js must be available on your server
  • Vue 3.2.13+ (or install
    @vue/server-renderer
    separately for older versions)
  • Vite Ruby for build configuration
  • 服务器上必须安装Node.js
  • Vue 3.2.13+(旧版本需单独安装
    @vue/server-renderer
  • 使用Vite Ruby进行构建配置

Setup Steps

配置步骤

1. Create SSR Entry Point

1. 创建SSR入口文件

Create
app/frontend/ssr/ssr.js
:
Vue 3:
javascript
import { createInertiaApp } from '@inertiajs/vue3'
import createServer from '@inertiajs/vue3/server'
import { renderToString } from '@vue/server-renderer'
import { createSSRApp, h } from 'vue'

const pages = import.meta.glob('../pages/**/*.vue', { eager: true })

createServer((page) =>
  createInertiaApp({
    page,
    render: renderToString,
    resolve: (name) => {
      const page = pages[`../pages/${name}.vue`]
      if (!page) {
        throw new Error(`Page not found: ${name}`)
      }
      return page
    },
    setup({ App, props, plugin }) {
      return createSSRApp({
        render: () => h(App, props),
      }).use(plugin)
    },
  })
)
React:
javascript
import { createInertiaApp } from '@inertiajs/react'
import createServer from '@inertiajs/react/server'
import ReactDOMServer from 'react-dom/server'

const pages = import.meta.glob('../pages/**/*.jsx', { eager: true })

createServer((page) =>
  createInertiaApp({
    page,
    render: ReactDOMServer.renderToString,
    resolve: (name) => {
      const page = pages[`../pages/${name}.jsx`]
      if (!page) {
        throw new Error(`Page not found: ${name}`)
      }
      return page
    },
    setup: ({ App, props }) => <App {...props} />,
  })
)
创建
app/frontend/ssr/ssr.js
文件:
Vue 3:
javascript
import { createInertiaApp } from '@inertiajs/vue3'
import createServer from '@inertiajs/vue3/server'
import { renderToString } from '@vue/server-renderer'
import { createSSRApp, h } from 'vue'

const pages = import.meta.glob('../pages/**/*.vue', { eager: true })

createServer((page) =>
  createInertiaApp({
    page,
    render: renderToString,
    resolve: (name) => {
      const page = pages[`../pages/${name}.vue`]
      if (!page) {
        throw new Error(`Page not found: ${name}`)
      }
      return page
    },
    setup({ App, props, plugin }) {
      return createSSRApp({
        render: () => h(App, props),
      }).use(plugin)
    },
  })
)
React:
javascript
import { createInertiaApp } from '@inertiajs/react'
import createServer from '@inertiajs/react/server'
import ReactDOMServer from 'react-dom/server'

const pages = import.meta.glob('../pages/**/*.jsx', { eager: true })

createServer((page) =>
  createInertiaApp({
    page,
    render: ReactDOMServer.renderToString,
    resolve: (name) => {
      const page = pages[`../pages/${name}.jsx`]
      if (!page) {
        throw new Error(`Page not found: ${name}`)
      }
      return page
    },
    setup: ({ App, props }) => <App {...props} />,
  })
)

2. Configure Vite Ruby

2. 配置Vite Ruby

Update
config/vite.json
:
json
{
  "all": {
    "sourceCodeDir": "app/frontend",
    "entrypointsDir": "entrypoints"
  },
  "development": {
    "autoBuild": true
  },
  "production": {
    "ssrBuildEnabled": true
  }
}
更新
config/vite.json
文件:
json
{
  "all": {
    "sourceCodeDir": "app/frontend",
    "entrypointsDir": "entrypoints"
  },
  "development": {
    "autoBuild": true
  },
  "production": {
    "ssrBuildEnabled": true
  }
}

3. Enable SSR in Rails

3. 在Rails中启用SSR

Update
config/initializers/inertia_rails.rb
:
ruby
InertiaRails.configure do |config|
  # Enable SSR only when Vite is configured for it
  config.ssr_enabled = ViteRuby.config.ssr_build_enabled

  # SSR server URL (default)
  config.ssr_url = 'http://localhost:13714'
end
更新
config/initializers/inertia_rails.rb
文件:
ruby
InertiaRails.configure do |config|
  # Enable SSR only when Vite is configured for it
  config.ssr_enabled = ViteRuby.config.ssr_build_enabled

  # SSR server URL (default)
  config.ssr_url = 'http://localhost:13714'
end

4. Update Client Entry Point

4. 更新客户端入口文件

Modify
app/frontend/entrypoints/application.js
for hydration:
Vue 3:
javascript
import { createInertiaApp } from '@inertiajs/vue3'
import { createSSRApp, h } from 'vue'

const pages = import.meta.glob('../pages/**/*.vue', { eager: true })

createInertiaApp({
  resolve: (name) => pages[`../pages/${name}.vue`],
  setup({ el, App, props, plugin }) {
    // Use createSSRApp instead of createApp for hydration
    createSSRApp({
      render: () => h(App, props),
    })
      .use(plugin)
      .mount(el)
  },
})
React:
javascript
import { createInertiaApp } from '@inertiajs/react'
import { hydrateRoot } from 'react-dom/client'

const pages = import.meta.glob('../pages/**/*.jsx', { eager: true })

createInertiaApp({
  resolve: (name) => pages[`../pages/${name}.jsx`],
  setup({ el, App, props }) {
    // Use hydrateRoot instead of createRoot for SSR
    hydrateRoot(el, <App {...props} />)
  },
})
修改
app/frontend/entrypoints/application.js
以支持水合渲染:
Vue 3:
javascript
import { createInertiaApp } from '@inertiajs/vue3'
import { createSSRApp, h } from 'vue'

const pages = import.meta.glob('../pages/**/*.vue', { eager: true })

createInertiaApp({
  resolve: (name) => pages[`../pages/${name}.vue`],
  setup({ el, App, props, plugin }) {
    // Use createSSRApp instead of createApp for hydration
    createSSRApp({
      render: () => h(App, props),
    })
      .use(plugin)
      .mount(el)
  },
})
React:
javascript
import { createInertiaApp } from '@inertiajs/react'
import { hydrateRoot } from 'react-dom/client'

const pages = import.meta.glob('../pages/**/*.jsx', { eager: true })

createInertiaApp({
  resolve: (name) => pages[`../pages/${name}.jsx`],
  setup({ el, App, props }) {
    // Use hydrateRoot instead of createRoot for SSR
    hydrateRoot(el, <App {...props} />)
  },
})

5. Update Layout for SSR Head

5. 在布局中添加SSR头部注入

Add SSR head injection to your layout:
erb
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csp_meta_tag %>
    <%= inertia_ssr_head %>
    <%= vite_client_tag %>
    <%= vite_javascript_tag 'application' %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>
erb
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csp_meta_tag %>
    <%= inertia_ssr_head %>
    <%= vite_client_tag %>
    <%= vite_javascript_tag 'application' %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

6. Build and Run

6. 构建并运行

bash
undefined
bash
undefined

Build both client and SSR bundles

Build both client and SSR bundles

bin/vite build bin/vite build --ssr
bin/vite build bin/vite build --ssr

Start the SSR server

Start the SSR server

bin/vite ssr
undefined
bin/vite ssr
undefined

Production Deployment

生产环境部署

Process Manager (systemd)

进程管理器(systemd)

Create
/etc/systemd/system/inertia-ssr.service
:
ini
[Unit]
Description=Inertia SSR Server
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/myapp/current
ExecStart=/usr/bin/node public/vite-ssr/ssr.js
Restart=on-failure
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target
bash
sudo systemctl enable inertia-ssr
sudo systemctl start inertia-ssr
创建
/etc/systemd/system/inertia-ssr.service
文件:
ini
[Unit]
Description=Inertia SSR Server
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/myapp/current
ExecStart=/usr/bin/node public/vite-ssr/ssr.js
Restart=on-failure
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target
bash
sudo systemctl enable inertia-ssr
sudo systemctl start inertia-ssr

Process Manager (PM2)

进程管理器(PM2)

bash
pm2 start public/vite-ssr/ssr.js --name inertia-ssr
pm2 save
bash
pm2 start public/vite-ssr/ssr.js --name inertia-ssr
pm2 save

Docker

Docker

dockerfile
undefined
dockerfile
undefined

Run SSR server alongside Rails

Run SSR server alongside Rails

CMD ["sh", "-c", "node public/vite-ssr/ssr.js & bundle exec puma"]
undefined
CMD ["sh", "-c", "node public/vite-ssr/ssr.js & bundle exec puma"]
undefined

Advanced Configuration

高级配置

Clustering

集群模式

For better performance on multi-core systems (requires
@inertiajs/core
v2.0.7+):
javascript
// ssr/ssr.js
createServer(
  (page) => createInertiaApp({ /* ... */ }),
  { cluster: true }  // Enable clustering
)
This runs multiple Node.js processes on a single port using round-robin request handling.
在多核系统中提升性能(需
@inertiajs/core
v2.0.7+):
javascript
// ssr/ssr.js
createServer(
  (page) => createInertiaApp({ /* ... */ }),
  { cluster: true }  // Enable clustering
)
此配置会使用轮询请求处理方式,在单个端口上运行多个Node.js进程。

Custom Port

自定义端口

javascript
createServer(
  (page) => createInertiaApp({ /* ... */ }),
  { port: 13715 }  // Custom port
)
Update Rails config to match:
ruby
config.ssr_url = 'http://localhost:13715'
javascript
createServer(
  (page) => createInertiaApp({ /* ... */ }),
  { port: 13715 }  // Custom port
)
同步更新Rails配置:
ruby
config.ssr_url = 'http://localhost:13715'

Conditional SSR

条件式SSR

Enable SSR only for specific routes:
ruby
class ApplicationController < ActionController::Base
  # Disable SSR for admin pages
  inertia_config(ssr_enabled: false)
end

class PagesController < ApplicationController
  # Enable SSR for public pages
  inertia_config(ssr_enabled: Rails.env.production?)
end
仅为特定路由启用SSR:
ruby
class ApplicationController < ActionController::Base
  # Disable SSR for admin pages
  inertia_config(ssr_enabled: false)
end

class PagesController < ApplicationController
  # Enable SSR for public pages
  inertia_config(ssr_enabled: Rails.env.production?)
end

Title and Meta Tags

标题与元标签

Server-Side Meta Tags

服务端元标签

Set meta tags in your controller:
ruby
class PostsController < ApplicationController
  def show
    post = Post.find(params[:id])

    render inertia: {
      post: post.as_json(only: [:id, :title, :content])
    }, meta: {
      title: post.title,
      description: post.excerpt,
      og_image: post.cover_image_url
    }
  end
end
在控制器中设置元标签:
ruby
class PostsController < ApplicationController
  def show
    post = Post.find(params[:id])

    render inertia: {
      post: post.as_json(only: [:id, :title, :content])
    }, meta: {
      title: post.title,
      description: post.excerpt,
      og_image: post.cover_image_url
    }
  end
end

Using Head Component

使用Head组件

Vue 3:
vue
<script setup>
import { Head } from '@inertiajs/vue3'

defineProps(['post'])
</script>

<template>
  <Head>
    <title>{{ post.title }}</title>
    <meta name="description" :content="post.excerpt" />
    <meta property="og:title" :content="post.title" />
    <meta property="og:image" :content="post.coverImageUrl" />
  </Head>

  <article>
    <h1>{{ post.title }}</h1>
    <!-- ... -->
  </article>
</template>
React:
jsx
import { Head } from '@inertiajs/react'

export default function Show({ post }) {
  return (
    <>
      <Head>
        <title>{post.title}</title>
        <meta name="description" content={post.excerpt} />
        <meta property="og:title" content={post.title} />
      </Head>

      <article>
        <h1>{post.title}</h1>
        {/* ... */}
      </article>
    </>
  )
}
Vue 3:
vue
<script setup>
import { Head } from '@inertiajs/vue3'

defineProps(['post'])
</script>

<template>
  <Head>
    <title>{{ post.title }}</title>
    <meta name="description" :content="post.excerpt" />
    <meta property="og:title" :content="post.title" />
    <meta property="og:image" :content="post.coverImageUrl" />
  </Head>

  <article>
    <h1>{{ post.title }}</h1>
    <!-- ... -->
  </article>
</template>
React:
jsx
import { Head } from '@inertiajs/react'

export default function Show({ post }) {
  return (
    <>
      <Head>
        <title>{post.title}</title>
        <meta name="description" content={post.excerpt} />
        <meta property="og:title" content={post.title} />
      </Head>

      <article>
        <h1>{post.title}</h1>
        {/* ... */}
      </article>
    </>
  )
}

Default Title Template

默认标题模板

javascript
createInertiaApp({
  title: (title) => title ? `${title} - My App` : 'My App',
  // ...
})
javascript
createInertiaApp({
  title: (title) => title ? `${title} - My App` : 'My App',
  // ...
})

Troubleshooting

故障排除

SSR Server Not Responding

SSR服务器无响应

  1. Check if the SSR server is running:
    curl http://localhost:13714
  2. Check logs:
    journalctl -u inertia-ssr -f
  3. Verify the port matches your Rails config
  1. 检查SSR服务器是否运行:
    curl http://localhost:13714
  2. 查看日志:
    journalctl -u inertia-ssr -f
  3. 验证端口是否与Rails配置一致

Hydration Mismatch Errors

水合渲染不匹配错误

  1. Ensure client and server render the same content
  2. Avoid browser-only APIs in initial render (use
    onMounted
    )
  3. Check for date/time formatting differences
javascript
// Bad - different on server vs client
const time = new Date().toLocaleTimeString()

// Good - render after mount
const time = ref(null)
onMounted(() => {
  time.value = new Date().toLocaleTimeString()
})
  1. 确保客户端与服务器渲染的内容一致
  2. 避免在初始渲染中使用仅浏览器可用的API(使用
    onMounted
  3. 检查日期/时间格式差异
javascript
// Bad - different on server vs client
const time = new Date().toLocaleTimeString()

// Good - render after mount
const time = ref(null)
onMounted(() => {
  time.value = new Date().toLocaleTimeString()
})

Memory Leaks

内存泄漏

  1. Avoid global state mutations in SSR
  2. Use request-scoped state only
  3. Monitor Node.js memory usage
  1. 避免在SSR中修改全局状态
  2. 仅使用请求作用域的状态
  3. 监控Node.js内存使用情况

Missing Styles on Initial Load

初始加载时样式缺失

Ensure CSS is included in the SSR bundle or use critical CSS extraction.
确保CSS已包含在SSR包中,或使用关键CSS提取。

Performance Tips

性能优化建议

  1. Keep SSR lightweight - Defer heavy computations to client
  2. Use streaming (when supported) for faster TTFB
  3. Cache SSR responses for static pages
  4. Monitor SSR latency - Add observability
ruby
undefined
  1. 保持SSR轻量化 - 将繁重计算延迟到客户端执行
  2. 使用流式渲染(支持时)以加快TTFB
  3. 缓存SSR响应(针对静态页面)
  4. 监控SSR延迟 - 添加可观测性
ruby
undefined

Log slow SSR renders

Log slow SSR renders

around_action :track_ssr_time, if: -> { request.inertia? }
def track_ssr_time start = Time.current yield duration = Time.current - start Rails.logger.info "SSR render: #{duration.round(3)}s" if duration > 0.1 end
undefined
around_action :track_ssr_time, if: -> { request.inertia? }
def track_ssr_time start = Time.current yield duration = Time.current - start Rails.logger.info "SSR render: #{duration.round(3)}s" if duration > 0.1 end
undefined