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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 744a98174d956b133ed9cfb4cdaa9fe931dcc5f4ddd0797797aeb201ace4f387
4
- data.tar.gz: 11ebab458200137d7ff996f11a2263854c2c62ef16ce8053f213045b96c374a5
3
+ metadata.gz: 61b47ecd00a6c6401d0689b67cf9944859c43e8f1f0cbae343c7b9f0865e5dc7
4
+ data.tar.gz: 96221231e4c6c3293c1c3b9c36fa0c2c338967703582262113781cec2cdd1336
5
5
  SHA512:
6
- metadata.gz: 9dd89b2e34c3c227ef792a4443c4eb6b93128542c3d409cafd5a78451e0863ae470636ef940062e77fdd423e18f03880d5e64214381c3b714fea99af5d7b1bc5
7
- data.tar.gz: 99006bff1b47f731a57b58bc7700553c13a364977dd4a9bba7d72f034c1b9725821e375d69973d868c12ce3bccc1cb3e078d29d0314ef1548cac52c821d5f37e
6
+ metadata.gz: 3a524d63eb673564622aa1329fc8c9ff8b47ea131f0785c79caf9a75e60f12912228f0f74dd7a44fe6a1958932f8514d214b539f3a32f7c62cf63d76b695669b
7
+ data.tar.gz: a423a79cc5e0d39ba58b039c7eec0d40b804625bc98e71bbeb57fe233b236f62794295daf6335efc5fd5d1ac1ae31cc19443ff10423c82b251360e5a4fa88bdf
data/README.md CHANGED
@@ -1,268 +1,246 @@
1
1
  # UniversalRenderer
2
2
 
3
- UniversalRenderer provides a streamlined way to integrate Server-Side Rendering (SSR) with an external SSR service into your Rails application. It helps you forward rendering requests, manage static or streaming responses, and improve performance, SEO, and user experience for JavaScript-heavy frontends.
3
+ A streamlined solution for integrating Server-Side Rendering (SSR) into Rails applications.
4
4
 
5
- ## Features
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
- ## Installation
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
- 1. Add this line to your application's Gemfile:
9
+ ## Features
16
10
 
17
- ```ruby
18
- gem "universal_renderer"
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
- 2. And then execute:
17
+ ## Installation
22
18
 
23
- ```bash
24
- $ bundle install
25
- ```
19
+ 1. Add to your Gemfile:
26
20
 
27
- 3. Run the install generator to create an initializer and include the necessary concern in your `ApplicationController`:
21
+ ```ruby
22
+ gem "universal_renderer"
23
+ ```
28
24
 
29
- ```bash
30
- $ rails generate universal_renderer:install
31
- ```
25
+ 2. Install:
32
26
 
33
- This will:
27
+ ```bash
28
+ $ bundle install
29
+ ```
34
30
 
35
- - Create `config/initializers/universal_renderer.rb`.
36
- - Add `include UniversalRenderer::Rendering` to your `app/controllers/application_controller.rb`.
31
+ 3. Run the generator:
32
+ ```bash
33
+ $ rails generate universal_renderer:install
34
+ ```
37
35
 
38
36
  ## Configuration
39
37
 
40
- Configure UniversalRenderer in `config/initializers/universal_renderer.rb`:
38
+ Configure in `config/initializers/universal_renderer.rb`:
41
39
 
42
40
  ```ruby
43
41
  UniversalRenderer.configure do |config|
44
- # (Required) The base URL of your SSR server.
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
- **Environment Variables:**
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
- Make sure any data passed is serializable to JSON (e.g., call `.as_json` on ActiveRecord objects).
48
+ After installation, you can pass data to your SSR service using `add_prop` in your controllers:
133
49
 
134
- ### Templates
135
-
136
- The gem relies on a few conventional template paths:
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
- Remember to yield `:head_tags` in your main layout's `<head>` section (e.g., `<%= yield :head_tags %>`) if you use `content_for` as in this example.
54
+ def show
55
+ @product = Product.find(params[:id])
162
56
 
163
- - `app/views/application/index.html.erb`: This template is used as a fallback if SSR fails (e.g., SSR server is down, returns an error, or `StaticClient` receives no data). This usually contains your client-side rendering (CSR) entry point.
57
+ # We can use the provided add_prop method to set a single value.
58
+ add_prop(:product, @product.as_json)
164
59
 
165
- ### View Helpers (`SsrHelpers`)
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
- The `UniversalRenderer::SsrHelpers` module provides helpers to mark locations in your HTML structure. For streaming to work correctly with a compatible Node.js SSR server (like `universal-renderer`), these helpers (or the raw HTML comments they produce) are crucial in your main Rails layout (e.g., `app/views/layouts/application.html.erb`).
64
+ fetch_ssr # or fetch on demand
168
65
 
169
- - `ssr_meta`: Placeholder for meta tags or other head elements generated by SSR. When used in a Rails layout for streaming, this helper outputs `<!-- SSR_META -->`, which the Node.js SSR server uses as an injection point.
170
- - `ssr_body`: Placeholder for the main root element where your SSR application will be rendered. When used in a Rails layout for streaming, this helper outputs `<!-- SSR_BODY -->`, which the Node.js SSR server uses to stream the main application content into.
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
- **Example (`app/views/layouts/application.html.erb` for streaming):**
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
- <!DOCTYPE html>
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
- ## SSR Server Expectations
194
-
195
- Your external SSR server needs to meet the following expectations:
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
- ```ruby
245
- # app/controllers/products_controller.rb
246
- class ProductsController < ApplicationController
247
- def show
248
- @product = Product.find(params[:id])
85
+ <% content_for :meta do %>
86
+ <%= sanitize_ssr @ssr[:meta] %>
87
+ <% end %>
249
88
 
250
- # Pass data to SSR service using add_props
251
- add_props(
252
- product: @product.as_json, # Ensure data is serializable
253
- related_products: @product.related_products.limit(5).as_json,
254
- )
89
+ <div id="root">
90
+ <%= sanitize_ssr @ssr[:styles] %>
91
+ <%= sanitize_ssr @ssr[:root] %>
92
+ </div>
255
93
 
256
- # default_render (from UniversalRenderer::Rendering) will be called automatically,
257
- # handling either streaming or static SSR based on configuration and environment.
258
- end
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
- TODO
242
+ Contributions are welcome! Please follow the coding guidelines in the project documentation.
265
243
 
266
244
  ## License
267
245
 
268
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
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