universal_renderer 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +204 -226
- data/lib/generators/universal_renderer/install_generator.rb +0 -8
- data/lib/universal_renderer/client/base.rb +75 -0
- data/lib/universal_renderer/client/stream/error_logger.rb +31 -0
- data/lib/universal_renderer/client/stream/execution.rb +94 -0
- data/lib/universal_renderer/client/stream/setup.rb +39 -0
- data/lib/universal_renderer/client/stream.rb +87 -0
- data/lib/universal_renderer/engine.rb +3 -0
- data/lib/universal_renderer/renderable.rb +162 -0
- data/lib/universal_renderer/ssr/helpers.rb +45 -0
- data/lib/universal_renderer/ssr/placeholders.rb +8 -0
- data/lib/universal_renderer/ssr/scrubber.rb +62 -0
- data/lib/universal_renderer/version.rb +1 -1
- data/lib/universal_renderer.rb +9 -1
- metadata +11 -16
- data/app/controllers/concerns/universal_renderer/rendering.rb +0 -132
- data/app/helpers/universal_renderer/ssr_helpers.rb +0 -19
- data/app/services/universal_renderer/static_client.rb +0 -56
- data/app/services/universal_renderer/stream_client/error_logger.rb +0 -29
- data/app/services/universal_renderer/stream_client/execution.rb +0 -88
- data/app/services/universal_renderer/stream_client/setup.rb +0 -37
- data/app/services/universal_renderer/stream_client.rb +0 -84
- data/lib/universal_renderer/ssr_scrubber.rb +0 -61
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 61b47ecd00a6c6401d0689b67cf9944859c43e8f1f0cbae343c7b9f0865e5dc7
|
4
|
+
data.tar.gz: 96221231e4c6c3293c1c3b9c36fa0c2c338967703582262113781cec2cdd1336
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3a524d63eb673564622aa1329fc8c9ff8b47ea131f0785c79caf9a75e60f12912228f0f74dd7a44fe6a1958932f8514d214b539f3a32f7c62cf63d76b695669b
|
7
|
+
data.tar.gz: a423a79cc5e0d39ba58b039c7eec0d40b804625bc98e71bbeb57fe233b236f62794295daf6335efc5fd5d1ac1ae31cc19443ff10423c82b251360e5a4fa88bdf
|
data/README.md
CHANGED
@@ -1,268 +1,246 @@
|
|
1
1
|
# UniversalRenderer
|
2
2
|
|
3
|
-
|
3
|
+
A streamlined solution for integrating Server-Side Rendering (SSR) into Rails applications.
|
4
4
|
|
5
|
-
##
|
6
|
-
|
7
|
-
- Supports both static and streaming SSR.
|
8
|
-
- Configurable SSR server endpoint and timeouts.
|
9
|
-
- Helper methods for passing data to your SSR service.
|
10
|
-
- Automatic fallback to client-side rendering if SSR fails.
|
11
|
-
- View helpers for integrating SSR content into your layouts.
|
5
|
+
## Overview
|
12
6
|
|
13
|
-
|
7
|
+
UniversalRenderer helps you forward rendering requests to external SSR services, manage responses, and improve performance, SEO, and user experience for JavaScript-heavy frontends. It works seamlessly with the `universal-renderer` NPM package.
|
14
8
|
|
15
|
-
|
9
|
+
## Features
|
16
10
|
|
17
|
-
|
18
|
-
|
19
|
-
|
11
|
+
- **Static and streaming SSR** support
|
12
|
+
- **Configurable SSR server endpoint** and timeouts
|
13
|
+
- **Simple API** for passing data between Rails and your SSR service
|
14
|
+
- **Automatic fallback** to client-side rendering if SSR fails
|
15
|
+
- **View helpers** for easy integration into your layouts
|
20
16
|
|
21
|
-
|
17
|
+
## Installation
|
22
18
|
|
23
|
-
|
24
|
-
$ bundle install
|
25
|
-
```
|
19
|
+
1. Add to your Gemfile:
|
26
20
|
|
27
|
-
|
21
|
+
```ruby
|
22
|
+
gem "universal_renderer"
|
23
|
+
```
|
28
24
|
|
29
|
-
|
30
|
-
$ rails generate universal_renderer:install
|
31
|
-
```
|
25
|
+
2. Install:
|
32
26
|
|
33
|
-
|
27
|
+
```bash
|
28
|
+
$ bundle install
|
29
|
+
```
|
34
30
|
|
35
|
-
|
36
|
-
|
31
|
+
3. Run the generator:
|
32
|
+
```bash
|
33
|
+
$ rails generate universal_renderer:install
|
34
|
+
```
|
37
35
|
|
38
36
|
## Configuration
|
39
37
|
|
40
|
-
Configure
|
38
|
+
Configure in `config/initializers/universal_renderer.rb`:
|
41
39
|
|
42
40
|
```ruby
|
43
41
|
UniversalRenderer.configure do |config|
|
44
|
-
|
45
|
-
# Example: 'http://localhost:3001' or 'https://your-ssr-service.com'
|
46
|
-
# Can also be set via the SSR_SERVER_URL environment variable.
|
47
|
-
config.ssr_url = ENV.fetch("SSR_SERVER_URL", "http://localhost:3001")
|
48
|
-
|
49
|
-
# (Optional) Timeout in seconds for requests to the SSR server.
|
50
|
-
# Defaults to 3 seconds.
|
51
|
-
# Can also be set via the SSR_TIMEOUT environment variable.
|
52
|
-
config.timeout = (ENV["SSR_TIMEOUT"] || 3).to_i
|
53
|
-
|
54
|
-
# (Optional) The path on your SSR server for streaming requests.
|
55
|
-
# Defaults to '/stream'.
|
56
|
-
# Can also be set via the SSR_STREAM_PATH environment variable.
|
57
|
-
config.ssr_stream_path = ENV.fetch("SSR_STREAM_PATH", "/stream")
|
42
|
+
config.ssr_url = "http://localhost:3001"
|
58
43
|
end
|
59
44
|
```
|
60
45
|
|
61
|
-
|
62
|
-
The `ssr_url`, `timeout`, and `ssr_stream_path` can be configured directly via environment variables (`SSR_SERVER_URL`, `SSR_TIMEOUT`, `SSR_STREAM_PATH`). If set, environment variables will take precedence over values in the initializer.
|
63
|
-
|
64
|
-
## Usage
|
65
|
-
|
66
|
-
The `universal_renderer:install` generator includes the `UniversalRenderer::Rendering` concern into your `ApplicationController`. This concern overrides `default_render` to automatically handle SSR.
|
67
|
-
|
68
|
-
### Rendering Flow Details
|
69
|
-
|
70
|
-
This gem facilitates communication with an external Node.js SSR server (like one built with the `universal-renderer` npm package).
|
71
|
-
|
72
|
-
**1. Static Rendering Flow:**
|
73
|
-
|
74
|
-
- When streaming is disabled or not applicable, the gem makes a `POST` request to the SSR server's static endpoint (by default, the root `/` of the `config.ssr_url`).
|
75
|
-
- The request includes the current URL and any props added via `add_props`.
|
76
|
-
- The SSR server is expected to return a JSON object (e.g., `{ "html_content": "...", "meta_tags": "...", "initial_state": { ... } }`).
|
77
|
-
- This JSON response is made available in your Rails view (typically `app/views/ssr/index.html.erb`) as the `@ssr` instance variable (with symbolized keys, e.g., `@ssr[:html_content]`).
|
78
|
-
|
79
|
-
**2. Streaming Rendering Flow:**
|
80
|
-
|
81
|
-
- **Layout Rendering & Splitting**:
|
82
|
-
- Your Rails application (e.g., `app/views/layouts/application.html.erb`) begins to render. This layout **must** use the `<%= ssr_meta %>` helper at the point where SSR meta tags should be injected (usually late in the `<head>`) and `<%= ssr_body %>` where the main SSR application body should appear.
|
83
|
-
- The `Rendering` concern captures the full layout string. It then splits this string at the `<!-- SSR_META -->` comment (generated by `<%= ssr_meta %>`).
|
84
|
-
- The portion of the layout _before_ `<!-- SSR_META -->` is immediately written to the HTTP response stream and sent to the client.
|
85
|
-
- **Request to SSR Server**:
|
86
|
-
- The gem then makes a `POST` request to the SSR server's streaming endpoint (configured by `config.ssr_stream_path`, default `/stream`).
|
87
|
-
- The JSON payload of this request includes:
|
88
|
-
- `url`: The current Rails request URL.
|
89
|
-
- `props`: Data passed from your Rails controller using `add_props`.
|
90
|
-
- `template`: This field contains the latter part of the rendered Rails view string, starting _directly with_ the `<!-- SSR_META -->` marker (generated by `<%= ssr_meta %>`) and continuing to the end of the document. This string must also include the `<!-- SSR_BODY -->` marker (generated by `<%= ssr_body %>`). The Node.js SSR server uses this `template` to construct the response around the streamed React application: it first sends the `<!-- SSR_META -->` part from the `template`, then injects its computed meta tags, then sends the HTML content found between `<!-- SSR_META -->` and `<!-- SSR_BODY -->` from the `template`. After this, the React application is streamed (filling the place of `<!-- SSR_BODY -->`), and finally, the portion of the `template` that was after `<!-- SSR_BODY -->` is appended.
|
91
|
-
- **Streaming Response**:
|
92
|
-
- The Node.js SSR server streams HTML content back. This content is piped directly into the Rails response stream, filling the placeholders in the `template` it received.
|
93
|
-
|
94
|
-
**Fallback:**
|
95
|
-
If SSR (either static or streaming) fails in a way that allows for it, the system will typically fall back to rendering `app/views/application/index.html.erb`, which should contain your client-side rendering (CSR) entry point.
|
96
|
-
|
97
|
-
### Passing Data to SSR (`add_props`)
|
98
|
-
|
99
|
-
In your controllers, you can use the `add_props` method to pass data from your Rails application to the SSR service. This data will be available as a JSON object under the `props` key in the JSON payload sent to your SSR service.
|
100
|
-
|
101
|
-
`add_props` can be called in two ways:
|
102
|
-
|
103
|
-
1. **Key-Value Pair:**
|
104
|
-
|
105
|
-
```ruby
|
106
|
-
# In your controller action
|
107
|
-
def show
|
108
|
-
@product = Product.find(params[:id])
|
109
|
-
add_props(:product, @product.as_json) # Ensure data is serializable
|
110
|
-
add_props(:current_user_name, current_user.name)
|
111
|
-
# ... default_render will be called implicitly
|
112
|
-
end
|
113
|
-
```
|
114
|
-
|
115
|
-
This will result in the `props` object in the SSR request payload being like:
|
116
|
-
`{ "product": { ...product_data... }, "current_user_name": "User Name" }`
|
117
|
-
|
118
|
-
2. **Hash Argument:**
|
119
|
-
|
120
|
-
```ruby
|
121
|
-
# In your controller action
|
122
|
-
def index
|
123
|
-
@posts = Post.recent
|
124
|
-
add_props(posts: @posts.map(&:as_json), current_page: params[:page])
|
125
|
-
# ... default_render will be called implicitly
|
126
|
-
end
|
127
|
-
```
|
128
|
-
|
129
|
-
This will result in the `props` object in the SSR request payload being like:
|
130
|
-
`{ "posts": [ ...posts_data... ], "current_page": "1" }`
|
46
|
+
## Basic Usage
|
131
47
|
|
132
|
-
|
48
|
+
After installation, you can pass data to your SSR service using `add_prop` in your controllers:
|
133
49
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
- `app/views/ssr/index.html.erb`: Used when `StaticClient` successfully receives data from the SSR server. This template typically uses the data (available in `@ssr`) to render the page.
|
139
|
-
**Example (`app/views/ssr/index.html.erb` for static rendering):**
|
140
|
-
This example assumes your SSR server returns a JSON object with keys like `:meta`, `:styles`, `:root`, and `:state`. You would need to ensure proper sanitization of this content (e.g., using `sanitize` or a custom helper like `sanitize_ssr` shown below).
|
141
|
-
|
142
|
-
```erb
|
143
|
-
<%# Assuming @ssr contains keys like :meta, :styles, :root, :state from the SSR server %>
|
144
|
-
<%# You are responsible for sanitizing these values appropriately. %>
|
145
|
-
<%# The sanitize_ssr helper is hypothetical and you'd need to implement it or use Rails' sanitize. %>
|
146
|
-
|
147
|
-
<% content_for :head_tags do %>
|
148
|
-
<%= raw @ssr[:meta] %> <%# Example: <meta name="description" content="..."> %>
|
149
|
-
<%= raw @ssr[:styles] %> <%# Example: <style>...</style> or <link rel="stylesheet" ...> %>
|
150
|
-
<% end %>
|
151
|
-
|
152
|
-
<div id="root" data-ssr-rendered="true">
|
153
|
-
<%= raw @ssr[:root] %> <%# Example: <div>Your pre-rendered React/Vue/etc. app</div> %>
|
154
|
-
</div>
|
155
|
-
|
156
|
-
<script id="ssr-state" type="application/json">
|
157
|
-
<%= raw @ssr[:state].to_json %>
|
158
|
-
</script>
|
159
|
-
```
|
50
|
+
```ruby
|
51
|
+
class ProductsController < ApplicationController
|
52
|
+
enable_ssr # enables SSR controller-wide
|
160
53
|
|
161
|
-
|
54
|
+
def show
|
55
|
+
@product = Product.find(params[:id])
|
162
56
|
|
163
|
-
|
57
|
+
# We can use the provided add_prop method to set a single value.
|
58
|
+
add_prop(:product, @product.as_json)
|
164
59
|
|
165
|
-
|
60
|
+
# We can use the provided push_prop method to push multiple values to an array.
|
61
|
+
# This is useful for pushing data to React Query.
|
62
|
+
push_prop(:query_data, { key: ["currentUser"], data: current_user.as_json })
|
166
63
|
|
167
|
-
|
64
|
+
fetch_ssr # or fetch on demand
|
168
65
|
|
169
|
-
|
170
|
-
|
66
|
+
# @ssr will now contain the SSR response, where the symbolized keys
|
67
|
+
# are the same keys returned by the SSR server response.
|
68
|
+
end
|
171
69
|
|
172
|
-
|
70
|
+
def default_render
|
71
|
+
# If you want to re-use the same layout across multiple actions.
|
72
|
+
# You can also put this in your ApplicationController.
|
73
|
+
render "ssr/index"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
```
|
173
77
|
|
174
78
|
```erb
|
175
|
-
|
176
|
-
<html>
|
177
|
-
<head>
|
178
|
-
<title>My App</title>
|
179
|
-
<%= csrf_meta_tags %>
|
180
|
-
<%= csp_meta_tag %>
|
181
|
-
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
182
|
-
<%= ssr_meta %> <%# SSR service might inject specific meta tags here %>
|
183
|
-
</head>
|
184
|
-
<body>
|
185
|
-
<div id="root">
|
186
|
-
<%= ssr_body %> <%# SSR service streams the main application content here %>
|
187
|
-
</div>
|
188
|
-
<%= javascript_importmap_tags %>
|
189
|
-
</body>
|
190
|
-
</html>
|
191
|
-
```
|
79
|
+
<%# "ssr/index" %>
|
192
80
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
1. **Static Rendering Endpoint (for `StaticClient`):**
|
198
|
-
|
199
|
-
- **Path:** Root path (`/`) of the `config.ssr_url`.
|
200
|
-
_Note: Some Node.js SSR servers like `universal-renderer` might default to a `/static` path. Ensure your SSR server listens on `/` for static requests if using this gem's default, or configure the Node.js server's `basePath` or routes accordingly._
|
201
|
-
- **Method:** `POST`
|
202
|
-
- **Request Body (JSON):**
|
203
|
-
```jsonc
|
204
|
-
{
|
205
|
-
"url": "current_rails_request_original_url",
|
206
|
-
"props": {
|
207
|
-
// JSON object built from add_props calls
|
208
|
-
// e.g., "product": { ... }, "current_user_name": "..."
|
209
|
-
},
|
210
|
-
}
|
211
|
-
```
|
212
|
-
- **Successful Response:** `200 OK` with a JSON body. The structure of this JSON is up to you, but it will be available in your `app/views/ssr/index.html.erb` template as `@ssr` (with keys symbolized). Example:
|
213
|
-
```jsonc
|
214
|
-
{
|
215
|
-
"meta": "<meta name='description' content='Pre-rendered page description'>\n<meta property='og:title' content='My SSR Page'>",
|
216
|
-
"styles": "<link rel='stylesheet' href='/path/to/ssr-specific-styles.css'>",
|
217
|
-
"root": "<div><h1>Hello from SSR!</h1><p>This is your pre-rendered application content.</p></div>",
|
218
|
-
"state": {
|
219
|
-
"productId": 123,
|
220
|
-
"initialData": { "foo": "bar" },
|
221
|
-
},
|
222
|
-
}
|
223
|
-
```
|
224
|
-
|
225
|
-
2. **Streaming Rendering Endpoint (for `StreamClient`):**
|
226
|
-
|
227
|
-
- **Path:** `config.ssr_stream_path` (defaults to `/stream`) on the `config.ssr_url`.
|
228
|
-
- **Method:** `POST`
|
229
|
-
- **Request Body (JSON):**
|
230
|
-
```jsonc
|
231
|
-
{
|
232
|
-
"url": "current_rails_request_original_url",
|
233
|
-
"props": {
|
234
|
-
// User-defined props from add_props calls,
|
235
|
-
// e.g., "product": { ... }, "current_user_name": "..."
|
236
|
-
},
|
237
|
-
"template": "<!-- SSR_META -->...HTML between meta and body...<!-- SSR_BODY -->...HTML after body...</html>",
|
238
|
-
}
|
239
|
-
```
|
240
|
-
- **Successful Response:** `200 OK` with `Content-Type: text/html`. The body should be an HTML stream. The Node.js SSR server will use the `template` field from the request body to inject content at the `<!-- SSR_META -->` and `<!-- SSR_BODY -->` placeholders.
|
241
|
-
|
242
|
-
## Example Controller
|
81
|
+
<%# Now you can use the instance variable @ssr in your layout. %>
|
82
|
+
<%# We'll send it with keys :meta, :styles, :root, and :state below. %>
|
83
|
+
<%# We can use the provided sanitize_ssr helper to sanitize our content %>
|
243
84
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
def show
|
248
|
-
@product = Product.find(params[:id])
|
85
|
+
<% content_for :meta do %>
|
86
|
+
<%= sanitize_ssr @ssr[:meta] %>
|
87
|
+
<% end %>
|
249
88
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
)
|
89
|
+
<div id="root">
|
90
|
+
<%= sanitize_ssr @ssr[:styles] %>
|
91
|
+
<%= sanitize_ssr @ssr[:root] %>
|
92
|
+
</div>
|
255
93
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
end
|
94
|
+
<script id="state" type="application/json">
|
95
|
+
<%= json_escape(@ssr[:state].to_json) %>
|
96
|
+
</script>
|
260
97
|
```
|
261
98
|
|
99
|
+
## Setting Up the SSR Server
|
100
|
+
|
101
|
+
To set up the SSR server for your Rails application:
|
102
|
+
|
103
|
+
1. Install the NPM package in your JavaScript project:
|
104
|
+
|
105
|
+
```bash
|
106
|
+
$ npm install universal-renderer
|
107
|
+
# or
|
108
|
+
$ yarn add universal-renderer
|
109
|
+
# or
|
110
|
+
$ bun add universal-renderer
|
111
|
+
```
|
112
|
+
|
113
|
+
2. Create a `setup` function at `app/frontend/ssr/setup.ts`:
|
114
|
+
|
115
|
+
```tsx
|
116
|
+
import {
|
117
|
+
HelmetProvider,
|
118
|
+
type HelmetDataContext,
|
119
|
+
} from "@dr.pogodin/react-helmet";
|
120
|
+
import { dehydrate, QueryClient, QueryClientProvider } from "react-query";
|
121
|
+
import { StaticRouter } from "react-router";
|
122
|
+
import { ServerStyleSheet } from "styled-components";
|
123
|
+
|
124
|
+
import App from "@/App";
|
125
|
+
import Metadata from "@/components/Metadata";
|
126
|
+
|
127
|
+
export default function setup(url: string, props: any) {
|
128
|
+
const pathname = new URL(url).pathname;
|
129
|
+
|
130
|
+
const helmetContext: HelmetDataContext = {};
|
131
|
+
const sheet = new ServerStyleSheet();
|
132
|
+
const queryClient = new QueryClient();
|
133
|
+
|
134
|
+
const { query_data } = props;
|
135
|
+
query_data.forEach(({ key, data }) => queryClient.setQueryData(key, data));
|
136
|
+
const state = dehydrate(queryClient);
|
137
|
+
|
138
|
+
const jsx = sheet.collectStyles(
|
139
|
+
<HelmetProvider context={helmetContext}>
|
140
|
+
<Metadata url={url} />
|
141
|
+
<QueryClientProvider client={queryClient}>
|
142
|
+
<StaticRouter location={pathname}>
|
143
|
+
<App />
|
144
|
+
</StaticRouter>
|
145
|
+
</QueryClientProvider>
|
146
|
+
</HelmetProvider>,
|
147
|
+
);
|
148
|
+
|
149
|
+
return { jsx, helmetContext, sheet, state, queryClient };
|
150
|
+
}
|
151
|
+
```
|
152
|
+
|
153
|
+
3. Update your `application.tsx` to hydrate the SSR state:
|
154
|
+
|
155
|
+
```tsx
|
156
|
+
import { HelmetProvider } from "@dr.pogodin/react-helmet";
|
157
|
+
import { createRoot, hydrateRoot } from "react-dom/client";
|
158
|
+
import { Hydrate, QueryClient, QueryClientProvider } from "react-query";
|
159
|
+
import { BrowserRouter } from "react-router";
|
160
|
+
|
161
|
+
import App from "@/App";
|
162
|
+
import Metadata from "@/components/Metadata";
|
163
|
+
|
164
|
+
const queryClient = new QueryClient();
|
165
|
+
const stateElement = document.getElementById("state")!;
|
166
|
+
const state = JSON.parse(stateElement.textContent);
|
167
|
+
|
168
|
+
const app = (
|
169
|
+
<HelmetProvider>
|
170
|
+
<Metadata url={window.location.href} />
|
171
|
+
<QueryClientProvider client={queryClient}>
|
172
|
+
<Hydrate state={state}>
|
173
|
+
<BrowserRouter>
|
174
|
+
<App />
|
175
|
+
</BrowserRouter>
|
176
|
+
</Hydrate>
|
177
|
+
</QueryClientProvider>
|
178
|
+
</HelmetProvider>
|
179
|
+
);
|
180
|
+
|
181
|
+
const rootElement = document.getElementById("root")!;
|
182
|
+
hydrateRoot(rootElement, app);
|
183
|
+
```
|
184
|
+
|
185
|
+
4. Create an SSR entry point at `app/frontend/ssr/ssr.ts`:
|
186
|
+
|
187
|
+
```ts
|
188
|
+
import { renderToString } from "react-dom/server";
|
189
|
+
import { createSsrServer } from "universal-renderer";
|
190
|
+
import { createServer as createViteServer } from "vite";
|
191
|
+
|
192
|
+
import setup from "@/ssr/setup";
|
193
|
+
|
194
|
+
const vite = await createViteServer({
|
195
|
+
server: { middlewareMode: true },
|
196
|
+
appType: "custom",
|
197
|
+
});
|
198
|
+
|
199
|
+
const app = await createSsrServer({
|
200
|
+
vite,
|
201
|
+
callbacks: {
|
202
|
+
// as typeof is a little hack to get the types to resolve correctly
|
203
|
+
// since Vite's ssrLoadModule doesn't include the types
|
204
|
+
setup: (await vite.ssrLoadModule("@/ssr/setup")).default as typeof setup,
|
205
|
+
render: async ({ jsx, helmetContext, sheet, state }) => {
|
206
|
+
const root = renderToString(jsx);
|
207
|
+
const meta = extractMeta(helmetContext);
|
208
|
+
const styles = sheet.getStyleTags();
|
209
|
+
return { meta, root, styles, state };
|
210
|
+
},
|
211
|
+
cleanup: async ({ sheet, queryClient }) => {
|
212
|
+
sheet?.seal();
|
213
|
+
queryClient?.clear();
|
214
|
+
},
|
215
|
+
onError: (error, context, errorContext) => {
|
216
|
+
vite.ssrFixStacktrace(error);
|
217
|
+
console.error(error);
|
218
|
+
},
|
219
|
+
},
|
220
|
+
});
|
221
|
+
|
222
|
+
app.listen(3001, () => {
|
223
|
+
console.log(`[SSR] server started on http://localhost:3001`);
|
224
|
+
});
|
225
|
+
```
|
226
|
+
|
227
|
+
5. Build the SSR bundle:
|
228
|
+
|
229
|
+
```bash
|
230
|
+
$ bin/vite build --ssr
|
231
|
+
```
|
232
|
+
|
233
|
+
6. Start your servers:
|
234
|
+
|
235
|
+
```Procfile
|
236
|
+
web: bin/rails s
|
237
|
+
ssr: bin/vite ssr
|
238
|
+
```
|
239
|
+
|
262
240
|
## Contributing
|
263
241
|
|
264
|
-
|
242
|
+
Contributions are welcome! Please follow the coding guidelines in the project documentation.
|
265
243
|
|
266
244
|
## License
|
267
245
|
|
268
|
-
|
246
|
+
Available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -5,13 +5,5 @@ module UniversalRenderer
|
|
5
5
|
def copy_initializer
|
6
6
|
template "initializer.rb", "config/initializers/universal_renderer.rb"
|
7
7
|
end
|
8
|
-
|
9
|
-
def include_concern
|
10
|
-
application_controller = "app/controllers/application_controller.rb"
|
11
|
-
|
12
|
-
inject_into_class application_controller,
|
13
|
-
"ApplicationController",
|
14
|
-
" include UniversalRenderer::Rendering\n"
|
15
|
-
end
|
16
8
|
end
|
17
9
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "json"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
module UniversalRenderer
|
8
|
+
module Client
|
9
|
+
# Fetches server-side rendered (SSR) content from the Node.js service
|
10
|
+
# in a single, blocking request. This client is used when SSR content
|
11
|
+
# is needed in its entirety before the Rails view rendering proceeds,
|
12
|
+
# as opposed to streaming SSR.
|
13
|
+
class Base
|
14
|
+
class << self
|
15
|
+
# Performs a POST request to the SSR service to retrieve the complete SSR content.
|
16
|
+
# This is used for non-streaming SSR, where the entire payload is fetched
|
17
|
+
# before the main application view is rendered.
|
18
|
+
#
|
19
|
+
# @param url [String] The URL of the page to render on the SSR server.
|
20
|
+
# This should typically be the `request.original_url` from the controller.
|
21
|
+
# @param props [Hash] A hash of props to be passed to the SSR service.
|
22
|
+
# These props will be available to the frontend components for rendering.
|
23
|
+
# @return [Hash, nil] The parsed JSON response from the SSR service with symbolized keys
|
24
|
+
# if the request is successful (HTTP 2xx). The structure of the hash depends
|
25
|
+
# on the SSR service implementation but typically includes keys like `:head`,
|
26
|
+
# `:body_html`, and `:body_attrs`.
|
27
|
+
# Returns `nil` if:
|
28
|
+
# - The `ssr_url` is not configured.
|
29
|
+
# - The request times out (open or read).
|
30
|
+
# - The SSR service returns a non-successful HTTP status code.
|
31
|
+
# - Any other `StandardError` occurs during the request.
|
32
|
+
# In case of failures, an error message is logged to `Rails.logger`.
|
33
|
+
def fetch(url, props)
|
34
|
+
ssr_url = UniversalRenderer.config.ssr_url
|
35
|
+
return if ssr_url.blank?
|
36
|
+
|
37
|
+
timeout = UniversalRenderer.config.timeout
|
38
|
+
|
39
|
+
begin
|
40
|
+
uri = URI.parse(ssr_url)
|
41
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
42
|
+
http.use_ssl = (uri.scheme == "https")
|
43
|
+
http.open_timeout = timeout
|
44
|
+
http.read_timeout = timeout
|
45
|
+
|
46
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
47
|
+
request.body = { url: url, props: props }.to_json
|
48
|
+
request["Content-Type"] = "application/json"
|
49
|
+
|
50
|
+
response = http.request(request)
|
51
|
+
|
52
|
+
if response.is_a?(Net::HTTPSuccess)
|
53
|
+
JSON.parse(response.body).deep_symbolize_keys
|
54
|
+
else
|
55
|
+
Rails.logger.error(
|
56
|
+
"SSR fetch request to #{ssr_url} failed: #{response.code} - #{response.message} (URL: #{url})"
|
57
|
+
)
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
61
|
+
Rails.logger.error(
|
62
|
+
"SSR fetch request to #{ssr_url} timed out: #{e.class.name} - #{e.message} (URL: #{url})"
|
63
|
+
)
|
64
|
+
nil
|
65
|
+
rescue StandardError => e
|
66
|
+
Rails.logger.error(
|
67
|
+
"SSR fetch request to #{ssr_url} failed: #{e.class.name} - #{e.message} (URL: #{url})"
|
68
|
+
)
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module UniversalRenderer
|
2
|
+
module Client
|
3
|
+
class Stream
|
4
|
+
module ErrorLogger
|
5
|
+
class << self
|
6
|
+
def _log_setup_error(error, target_uri_string)
|
7
|
+
backtrace_info = error.backtrace&.first || "No backtrace available"
|
8
|
+
Rails.logger.error(
|
9
|
+
"Unexpected error during SSR stream setup for #{target_uri_string}: " \
|
10
|
+
"#{error.class.name} - #{error.message} at #{backtrace_info}"
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
def _log_connection_error(error, target_uri_string)
|
15
|
+
Rails.logger.error(
|
16
|
+
"SSR stream connection to #{target_uri_string} failed: #{error.class.name} - #{error.message}"
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def _log_unexpected_error(error, target_uri_string, context_message)
|
21
|
+
backtrace_info = error.backtrace&.first || "No backtrace available"
|
22
|
+
Rails.logger.error(
|
23
|
+
"#{context_message} for #{target_uri_string}: " \
|
24
|
+
"#{error.class.name} - #{error.message} at #{backtrace_info}"
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|