universal_renderer 0.2.4
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +268 -0
- data/Rakefile +8 -0
- data/app/controllers/concerns/universal_renderer/rendering.rb +132 -0
- data/app/helpers/universal_renderer/ssr_helpers.rb +19 -0
- data/app/services/universal_renderer/static_client.rb +56 -0
- data/app/services/universal_renderer/stream_client/error_logger.rb +29 -0
- data/app/services/universal_renderer/stream_client/execution.rb +88 -0
- data/app/services/universal_renderer/stream_client/setup.rb +37 -0
- data/app/services/universal_renderer/stream_client.rb +84 -0
- data/config/routes.rb +1 -0
- data/lib/generators/universal_renderer/install_generator.rb +17 -0
- data/lib/generators/universal_renderer/templates/initializer.rb +4 -0
- data/lib/tasks/universal_renderer_tasks.rake +4 -0
- data/lib/universal_renderer/configuration.rb +11 -0
- data/lib/universal_renderer/engine.rb +4 -0
- data/lib/universal_renderer/ssr_scrubber.rb +61 -0
- data/lib/universal_renderer/version.rb +3 -0
- data/lib/universal_renderer.rb +18 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 744a98174d956b133ed9cfb4cdaa9fe931dcc5f4ddd0797797aeb201ace4f387
|
4
|
+
data.tar.gz: 11ebab458200137d7ff996f11a2263854c2c62ef16ce8053f213045b96c374a5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9dd89b2e34c3c227ef792a4443c4eb6b93128542c3d409cafd5a78451e0863ae470636ef940062e77fdd423e18f03880d5e64214381c3b714fea99af5d7b1bc5
|
7
|
+
data.tar.gz: 99006bff1b47f731a57b58bc7700553c13a364977dd4a9bba7d72f034c1b9725821e375d69973d868c12ce3bccc1cb3e078d29d0314ef1548cac52c821d5f37e
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright thaske
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
# UniversalRenderer
|
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.
|
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.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
1. Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem "universal_renderer"
|
19
|
+
```
|
20
|
+
|
21
|
+
2. And then execute:
|
22
|
+
|
23
|
+
```bash
|
24
|
+
$ bundle install
|
25
|
+
```
|
26
|
+
|
27
|
+
3. Run the install generator to create an initializer and include the necessary concern in your `ApplicationController`:
|
28
|
+
|
29
|
+
```bash
|
30
|
+
$ rails generate universal_renderer:install
|
31
|
+
```
|
32
|
+
|
33
|
+
This will:
|
34
|
+
|
35
|
+
- Create `config/initializers/universal_renderer.rb`.
|
36
|
+
- Add `include UniversalRenderer::Rendering` to your `app/controllers/application_controller.rb`.
|
37
|
+
|
38
|
+
## Configuration
|
39
|
+
|
40
|
+
Configure UniversalRenderer in `config/initializers/universal_renderer.rb`:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
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")
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
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" }`
|
131
|
+
|
132
|
+
Make sure any data passed is serializable to JSON (e.g., call `.as_json` on ActiveRecord objects).
|
133
|
+
|
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
|
+
```
|
160
|
+
|
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.
|
162
|
+
|
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.
|
164
|
+
|
165
|
+
### View Helpers (`SsrHelpers`)
|
166
|
+
|
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`).
|
168
|
+
|
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.
|
171
|
+
|
172
|
+
**Example (`app/views/layouts/application.html.erb` for streaming):**
|
173
|
+
|
174
|
+
```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
|
+
```
|
192
|
+
|
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
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
# app/controllers/products_controller.rb
|
246
|
+
class ProductsController < ApplicationController
|
247
|
+
def show
|
248
|
+
@product = Product.find(params[:id])
|
249
|
+
|
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
|
+
)
|
255
|
+
|
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
|
260
|
+
```
|
261
|
+
|
262
|
+
## Contributing
|
263
|
+
|
264
|
+
TODO
|
265
|
+
|
266
|
+
## License
|
267
|
+
|
268
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
module UniversalRenderer
|
2
|
+
module Rendering
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include ActionController::Live
|
7
|
+
helper UniversalRenderer::SsrHelpers
|
8
|
+
before_action :initialize_props
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def default_render
|
14
|
+
return super unless request.format.html?
|
15
|
+
if use_ssr_streaming?
|
16
|
+
render_ssr_stream
|
17
|
+
else
|
18
|
+
@ssr =
|
19
|
+
UniversalRenderer::StaticClient.static(
|
20
|
+
request.original_url,
|
21
|
+
@universal_renderer_props
|
22
|
+
)
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def render_ssr_stream
|
28
|
+
set_streaming_headers
|
29
|
+
|
30
|
+
full_layout = render_to_string
|
31
|
+
|
32
|
+
split_index = full_layout.index("<!-- SSR_META -->")
|
33
|
+
before_meta = full_layout[0...split_index]
|
34
|
+
after_meta = full_layout[split_index..]
|
35
|
+
|
36
|
+
response.stream.write(before_meta)
|
37
|
+
|
38
|
+
current_props = @universal_renderer_props.dup
|
39
|
+
|
40
|
+
streaming_succeeded =
|
41
|
+
UniversalRenderer::StreamClient.stream(
|
42
|
+
request.original_url,
|
43
|
+
current_props,
|
44
|
+
after_meta,
|
45
|
+
response
|
46
|
+
)
|
47
|
+
|
48
|
+
handle_ssr_stream_fallback(response) unless streaming_succeeded
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize_props
|
52
|
+
@universal_renderer_props = {}
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_props(key_or_hash, data_value = nil)
|
56
|
+
if data_value.nil? && key_or_hash.is_a?(Hash)
|
57
|
+
@universal_renderer_props.merge!(key_or_hash.deep_stringify_keys)
|
58
|
+
else
|
59
|
+
@universal_renderer_props[key_or_hash.to_s] = data_value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Allows a prop to be treated as an array, pushing new values to it.
|
64
|
+
# If the prop does not exist or is nil, it's initialized as an array.
|
65
|
+
# If the prop exists but is not an array (e.g., set as a scalar by `add_props`),
|
66
|
+
# its current value will be converted into the first element of the new array.
|
67
|
+
# If `value_to_add` is an array, its elements are concatenated. Otherwise, `value_to_add` is appended.
|
68
|
+
def push_prop(key, value_to_add)
|
69
|
+
prop_key = key.to_s
|
70
|
+
current_value = @universal_renderer_props[prop_key]
|
71
|
+
|
72
|
+
if current_value.nil?
|
73
|
+
@universal_renderer_props[prop_key] = []
|
74
|
+
elsif !current_value.is_a?(Array)
|
75
|
+
@universal_renderer_props[prop_key] = [current_value]
|
76
|
+
end
|
77
|
+
# At this point, @universal_renderer_props[prop_key] is guaranteed to be an array.
|
78
|
+
|
79
|
+
if value_to_add.is_a?(Array)
|
80
|
+
@universal_renderer_props[prop_key].concat(value_to_add)
|
81
|
+
else
|
82
|
+
@universal_renderer_props[prop_key] << value_to_add
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def use_ssr_streaming?
|
87
|
+
%w[1 true yes y].include?(ENV["ENABLE_SSR_STREAMING"]&.downcase)
|
88
|
+
end
|
89
|
+
|
90
|
+
def set_streaming_headers
|
91
|
+
# Tell Cloudflare / proxies not to cache or buffer.
|
92
|
+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
93
|
+
response.headers["Pragma"] = "no-cache"
|
94
|
+
response.headers["Expires"] = "0"
|
95
|
+
|
96
|
+
# Disable Nginx buffering per-response.
|
97
|
+
response.headers["X-Accel-Buffering"] = "no"
|
98
|
+
response.headers["Content-Type"] = "text/html"
|
99
|
+
|
100
|
+
# Remove Content-Length header to prevent buffering.
|
101
|
+
response.headers.delete("Content-Length")
|
102
|
+
end
|
103
|
+
|
104
|
+
def handle_ssr_stream_fallback(response)
|
105
|
+
# SSR streaming failed or was not possible (e.g. server down, config missing).
|
106
|
+
# Ensure response hasn't been touched in a way that prevents a new render.
|
107
|
+
return unless response.committed? || response.body.present?
|
108
|
+
|
109
|
+
Rails.logger.error(
|
110
|
+
"SSR stream fallback:" \
|
111
|
+
"Cannot render default fallback template because response was already committed or body present."
|
112
|
+
)
|
113
|
+
# Close the stream if it's still open to prevent client connection from hanging
|
114
|
+
# when we can't render a fallback page due to already committed response
|
115
|
+
response.stream.close unless response.stream.closed?
|
116
|
+
# If response not committed, no explicit render is called here,
|
117
|
+
# allowing Rails' default rendering behavior to take over.
|
118
|
+
end
|
119
|
+
|
120
|
+
# Overrides the built-in render_to_string.
|
121
|
+
# If you call render_to_string with no explicit template/partial/inline,
|
122
|
+
# it will fall back to 'ssr/index'.
|
123
|
+
def render_to_string(options = {}, *args, &block)
|
124
|
+
if options.is_a?(Hash) && !options.key?(:template) &&
|
125
|
+
!options.key?(:partial) && !options.key?(:inline) &&
|
126
|
+
!options.key?(:json) && !options.key?(:xml)
|
127
|
+
options = options.merge(template: "application/index")
|
128
|
+
end
|
129
|
+
super(options, *args, &block)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module UniversalRenderer
|
2
|
+
module SsrHelpers
|
3
|
+
def ssr_meta
|
4
|
+
"<!-- SSR_META -->".html_safe
|
5
|
+
end
|
6
|
+
|
7
|
+
def ssr_body
|
8
|
+
"<!-- SSR_BODY -->".html_safe
|
9
|
+
end
|
10
|
+
|
11
|
+
def sanitize_ssr(html)
|
12
|
+
sanitize(html, scrubber: SsrScrubber.new)
|
13
|
+
end
|
14
|
+
|
15
|
+
def use_ssr_streaming?
|
16
|
+
%w[1 true yes y].include?(ENV["ENABLE_SSR_STREAMING"]&.downcase)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "json" # Ensure JSON is required for parsing and generation
|
5
|
+
require "uri" # Ensure URI is required for URI parsing
|
6
|
+
|
7
|
+
module UniversalRenderer
|
8
|
+
class StaticClient
|
9
|
+
class << self
|
10
|
+
# Performs a POST request to the SSR service to get statically rendered content.
|
11
|
+
#
|
12
|
+
# @param url [String] The URL of the page to render.
|
13
|
+
# @param props [Hash] Additional data to be passed for rendering (renamed from query_data for clarity).
|
14
|
+
# @return [Hash, nil] The parsed JSON response as symbolized keys if successful, otherwise nil.
|
15
|
+
def static(url, props)
|
16
|
+
ssr_url = UniversalRenderer.config.ssr_url
|
17
|
+
return if ssr_url.blank?
|
18
|
+
|
19
|
+
timeout = UniversalRenderer.config.timeout
|
20
|
+
|
21
|
+
begin
|
22
|
+
uri = URI.parse(ssr_url)
|
23
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
24
|
+
http.use_ssl = (uri.scheme == "https")
|
25
|
+
http.open_timeout = timeout
|
26
|
+
http.read_timeout = timeout
|
27
|
+
|
28
|
+
request = Net::HTTP::Post.new(uri.request_uri) # Use uri.request_uri to include path if present in ssr_url
|
29
|
+
request.body = { url: url, props: props }.to_json
|
30
|
+
request["Content-Type"] = "application/json"
|
31
|
+
|
32
|
+
response = http.request(request)
|
33
|
+
|
34
|
+
if response.is_a?(Net::HTTPSuccess)
|
35
|
+
JSON.parse(response.body).deep_symbolize_keys
|
36
|
+
else
|
37
|
+
Rails.logger.error(
|
38
|
+
"SSR static request to #{ssr_url} failed: #{response.code} - #{response.message} (URL: #{url})"
|
39
|
+
)
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
43
|
+
Rails.logger.error(
|
44
|
+
"SSR static request to #{ssr_url} timed out: #{e.class.name} - #{e.message} (URL: #{url})"
|
45
|
+
)
|
46
|
+
nil
|
47
|
+
rescue StandardError => e
|
48
|
+
Rails.logger.error(
|
49
|
+
"SSR static request to #{ssr_url} failed: #{e.class.name} - #{e.message} (URL: #{url})"
|
50
|
+
)
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module UniversalRenderer
|
2
|
+
class StreamClient
|
3
|
+
module ErrorLogger
|
4
|
+
class << self
|
5
|
+
def _log_setup_error(error, target_uri_string)
|
6
|
+
backtrace_info = error.backtrace&.first || "No backtrace available"
|
7
|
+
Rails.logger.error(
|
8
|
+
"Unexpected error during SSR stream setup for #{target_uri_string}: " \
|
9
|
+
"#{error.class.name} - #{error.message} at #{backtrace_info}"
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
def _log_connection_error(error, target_uri_string)
|
14
|
+
Rails.logger.error(
|
15
|
+
"SSR stream connection to #{target_uri_string} failed: #{error.class.name} - #{error.message}"
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def _log_unexpected_error(error, target_uri_string, context_message)
|
20
|
+
backtrace_info = error.backtrace&.first || "No backtrace available"
|
21
|
+
Rails.logger.error(
|
22
|
+
"#{context_message} for #{target_uri_string}: " \
|
23
|
+
"#{error.class.name} - #{error.message} at #{backtrace_info}"
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module UniversalRenderer
|
2
|
+
class StreamClient
|
3
|
+
module Execution
|
4
|
+
class << self
|
5
|
+
def _perform_streaming(
|
6
|
+
http_client,
|
7
|
+
http_post_request,
|
8
|
+
response,
|
9
|
+
stream_uri
|
10
|
+
)
|
11
|
+
http_client.request(http_post_request) do |node_res|
|
12
|
+
Execution._handle_node_response_streaming(
|
13
|
+
node_res,
|
14
|
+
response,
|
15
|
+
stream_uri
|
16
|
+
)
|
17
|
+
|
18
|
+
return true
|
19
|
+
end
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
def _handle_node_response_streaming(node_res, response, stream_uri)
|
24
|
+
if node_res.is_a?(Net::HTTPSuccess)
|
25
|
+
Execution._stream_response_body(node_res, response.stream)
|
26
|
+
else
|
27
|
+
Execution._handle_streaming_for_node_error(
|
28
|
+
node_res,
|
29
|
+
response,
|
30
|
+
stream_uri
|
31
|
+
)
|
32
|
+
end
|
33
|
+
rescue StandardError => e
|
34
|
+
Rails.logger.error(
|
35
|
+
"Error during SSR data transfer or stream writing from #{stream_uri}: #{e.class.name} - #{e.message}"
|
36
|
+
)
|
37
|
+
|
38
|
+
Execution._write_generic_html_error(
|
39
|
+
response.stream,
|
40
|
+
"Streaming Error",
|
41
|
+
"<p>A problem occurred while loading content. Please refresh.</p>"
|
42
|
+
)
|
43
|
+
ensure
|
44
|
+
response.stream.close unless response.stream.closed?
|
45
|
+
end
|
46
|
+
|
47
|
+
def _stream_response_body(source_http_response, target_io_stream)
|
48
|
+
source_http_response.read_body do |chunk|
|
49
|
+
target_io_stream.write(chunk)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def _handle_streaming_for_node_error(node_res, response, stream_uri)
|
54
|
+
Rails.logger.error(
|
55
|
+
"SSR stream server at #{stream_uri} responded with #{node_res.code} #{node_res.message}."
|
56
|
+
)
|
57
|
+
|
58
|
+
is_potentially_viewable_error =
|
59
|
+
node_res["Content-Type"]&.match?(%r{text/html}i)
|
60
|
+
|
61
|
+
if is_potentially_viewable_error
|
62
|
+
Rails.logger.info(
|
63
|
+
"Attempting to stream HTML error page from Node SSR server."
|
64
|
+
)
|
65
|
+
Execution._stream_response_body(node_res, response.stream)
|
66
|
+
else
|
67
|
+
Rails.logger.warn(
|
68
|
+
"Node SSR server error response Content-Type ('#{node_res["Content-Type"]}') is not text/html. " \
|
69
|
+
"Injecting generic error message into the stream."
|
70
|
+
)
|
71
|
+
|
72
|
+
Execution._write_generic_html_error(
|
73
|
+
response.stream,
|
74
|
+
"Application Error",
|
75
|
+
"<p>There was an issue rendering this page. Please try again later.</p>"
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def _write_generic_html_error(stream, title_text, message_html_fragment)
|
81
|
+
return if stream.closed?
|
82
|
+
|
83
|
+
stream.write("<h1>#{title_text}</h1>#{message_html_fragment}")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module UniversalRenderer
|
2
|
+
class StreamClient
|
3
|
+
module Setup
|
4
|
+
class << self
|
5
|
+
def _ensure_ssr_server_url_configured?(config)
|
6
|
+
config.ssr_url.present?
|
7
|
+
end
|
8
|
+
|
9
|
+
def _build_stream_request_components(body, config)
|
10
|
+
# Ensure ssr_url is present, though _ensure_ssr_server_url_configured? should have caught this.
|
11
|
+
# However, direct calls to this method might occur, so a check or reliance on config.ssr_url is important.
|
12
|
+
if config.ssr_url.blank?
|
13
|
+
raise ArgumentError, "SSR URL is not configured."
|
14
|
+
end
|
15
|
+
|
16
|
+
parsed_ssr_url = URI.parse(config.ssr_url)
|
17
|
+
stream_uri = URI.join(parsed_ssr_url, config.ssr_stream_path)
|
18
|
+
|
19
|
+
http = Net::HTTP.new(stream_uri.host, stream_uri.port)
|
20
|
+
http.use_ssl = (stream_uri.scheme == "https")
|
21
|
+
http.open_timeout = config.timeout
|
22
|
+
http.read_timeout = config.timeout
|
23
|
+
|
24
|
+
http_request =
|
25
|
+
Net::HTTP::Post.new(
|
26
|
+
stream_uri.request_uri,
|
27
|
+
"Content-Type" => "application/json"
|
28
|
+
)
|
29
|
+
|
30
|
+
http_request.body = body.to_json
|
31
|
+
|
32
|
+
[stream_uri, http, http_request]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "stream_client/setup"
|
4
|
+
require_relative "stream_client/execution"
|
5
|
+
require_relative "stream_client/error_logger"
|
6
|
+
|
7
|
+
module UniversalRenderer
|
8
|
+
class StreamClient
|
9
|
+
extend Setup
|
10
|
+
extend Execution
|
11
|
+
extend ErrorLogger
|
12
|
+
|
13
|
+
# Orchestrates the streaming process for server-side rendering.
|
14
|
+
#
|
15
|
+
# @param url [String] The URL of the page to render.
|
16
|
+
# @param props [Hash] Data to be passed for rendering, including layout HTML.
|
17
|
+
# @param template [String] The HTML template to use for rendering.
|
18
|
+
# @param response [ActionDispatch::Response] The Rails response object to stream to.
|
19
|
+
# @return [Boolean] True if streaming was initiated, false otherwise.
|
20
|
+
def self.stream(url, props, template, response)
|
21
|
+
config = UniversalRenderer.config
|
22
|
+
|
23
|
+
unless Setup._ensure_ssr_server_url_configured?(config)
|
24
|
+
Rails.logger.warn(
|
25
|
+
"StreamClient: SSR URL (config.ssr_url) is not configured. Falling back."
|
26
|
+
)
|
27
|
+
return false
|
28
|
+
end
|
29
|
+
|
30
|
+
stream_uri_obj = nil
|
31
|
+
full_ssr_url_for_log = config.ssr_url.to_s # For logging in case of early error
|
32
|
+
|
33
|
+
begin
|
34
|
+
body = { url: url, props: props, template: template }
|
35
|
+
|
36
|
+
actual_stream_uri, http_client, http_post_request =
|
37
|
+
Setup._build_stream_request_components(body, config)
|
38
|
+
|
39
|
+
stream_uri_obj = actual_stream_uri
|
40
|
+
|
41
|
+
full_ssr_url_for_log = actual_stream_uri.to_s # Update for more specific logging
|
42
|
+
rescue URI::InvalidURIError => e
|
43
|
+
Rails.logger.error(
|
44
|
+
"StreamClient: SSR stream failed due to invalid URI ('#{config.ssr_url}'): #{e.message}"
|
45
|
+
)
|
46
|
+
|
47
|
+
return false
|
48
|
+
rescue StandardError => e
|
49
|
+
_log_setup_error(e, full_ssr_url_for_log)
|
50
|
+
|
51
|
+
return false
|
52
|
+
end
|
53
|
+
|
54
|
+
Execution._perform_streaming(
|
55
|
+
http_client,
|
56
|
+
http_post_request,
|
57
|
+
response,
|
58
|
+
stream_uri_obj
|
59
|
+
)
|
60
|
+
rescue Errno::ECONNREFUSED,
|
61
|
+
Errno::EHOSTUNREACH,
|
62
|
+
Net::OpenTimeout,
|
63
|
+
Net::ReadTimeout,
|
64
|
+
SocketError => e
|
65
|
+
uri_str_for_conn_error =
|
66
|
+
stream_uri_obj ? stream_uri_obj.to_s : full_ssr_url_for_log
|
67
|
+
|
68
|
+
ErrorLogger._log_connection_error(e, uri_str_for_conn_error)
|
69
|
+
|
70
|
+
false
|
71
|
+
rescue StandardError => e
|
72
|
+
uri_str_for_unexpected_error =
|
73
|
+
stream_uri_obj ? stream_uri_obj.to_s : full_ssr_url_for_log
|
74
|
+
|
75
|
+
ErrorLogger._log_unexpected_error(
|
76
|
+
e,
|
77
|
+
uri_str_for_unexpected_error,
|
78
|
+
"StreamClient: Unexpected error during SSR stream process"
|
79
|
+
)
|
80
|
+
|
81
|
+
false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Rails.application.routes.draw {}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module UniversalRenderer
|
2
|
+
class InstallGenerator < Rails::Generators::Base
|
3
|
+
source_root File.expand_path("templates", __dir__)
|
4
|
+
|
5
|
+
def copy_initializer
|
6
|
+
template "initializer.rb", "config/initializers/universal_renderer.rb"
|
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
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module UniversalRenderer
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :ssr_url, :timeout, :ssr_stream_path
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@ssr_url = ENV.fetch("SSR_SERVER_URL", nil)
|
7
|
+
@timeout = (ENV["SSR_TIMEOUT"] || 3).to_i
|
8
|
+
@ssr_stream_path = ENV.fetch("SSR_STREAM_PATH", "/stream")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require "loofah"
|
2
|
+
|
3
|
+
module UniversalRenderer
|
4
|
+
class SsrScrubber < ::Loofah::Scrubber
|
5
|
+
def initialize
|
6
|
+
super
|
7
|
+
@direction = :top_down
|
8
|
+
end
|
9
|
+
|
10
|
+
def scrub(node)
|
11
|
+
# Primary actions: stop if script, continue if a passthrough tag, otherwise clean attributes.
|
12
|
+
return Loofah::Scrubber::STOP if handle_script_node(node) # Checks for <script> and removes it
|
13
|
+
|
14
|
+
# Allows <link rel="stylesheet">, <style>, <meta> to pass through this scrubber.
|
15
|
+
return Loofah::Scrubber::CONTINUE if passthrough_node?(node)
|
16
|
+
|
17
|
+
# For all other nodes, clean potentially harmful attributes.
|
18
|
+
clean_attributes(node)
|
19
|
+
# Default Loofah behavior (CONTINUE for children) applies if not returned earlier.
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# Handles <script> tags: removes them and returns true if a script node was processed.
|
25
|
+
def handle_script_node(node)
|
26
|
+
return false unless node.name == "script"
|
27
|
+
|
28
|
+
node.remove
|
29
|
+
true # Indicates the node was a script and has been handled.
|
30
|
+
end
|
31
|
+
|
32
|
+
# Checks if the node is a type that should bypass detailed attribute scrubbing.
|
33
|
+
def passthrough_node?(node)
|
34
|
+
(node.name == "link" && node["rel"]&.to_s&.downcase == "stylesheet") ||
|
35
|
+
%w[style meta].include?(node.name)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Orchestrates the cleaning of attributes for a given node.
|
39
|
+
def clean_attributes(node)
|
40
|
+
remove_javascript_href(node)
|
41
|
+
remove_event_handlers(node)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Removes "javascript:" hrefs from <a> tags.
|
45
|
+
def remove_javascript_href(node)
|
46
|
+
if node.name == "a" &&
|
47
|
+
node["href"]&.to_s&.downcase&.start_with?("javascript:")
|
48
|
+
node.remove_attribute("href")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Removes "on*" event handler attributes from any node.
|
53
|
+
def remove_event_handlers(node)
|
54
|
+
attrs_to_remove =
|
55
|
+
node.attributes.keys.select do |name|
|
56
|
+
name.to_s.downcase.start_with?("on")
|
57
|
+
end
|
58
|
+
attrs_to_remove.each { |attr_name| node.remove_attribute(attr_name) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "universal_renderer/version"
|
2
|
+
require "universal_renderer/engine"
|
3
|
+
require "universal_renderer/configuration"
|
4
|
+
require "universal_renderer/ssr_scrubber"
|
5
|
+
|
6
|
+
module UniversalRenderer
|
7
|
+
class << self
|
8
|
+
attr_writer :config
|
9
|
+
|
10
|
+
def config
|
11
|
+
@config ||= Configuration.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def configure
|
15
|
+
yield(config)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: universal_renderer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- thaske
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-05-20 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: loofah
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '2.24'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '2.24'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rails
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '7.1'
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 7.1.5.1
|
36
|
+
type: :runtime
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - "~>"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '7.1'
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 7.1.5.1
|
46
|
+
description: Provides helper methods and configuration to forward rendering requests
|
47
|
+
from a Rails app to an external SSR server and return the response.
|
48
|
+
email:
|
49
|
+
- 10328778+thaske@users.noreply.github.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- MIT-LICENSE
|
55
|
+
- README.md
|
56
|
+
- Rakefile
|
57
|
+
- app/controllers/concerns/universal_renderer/rendering.rb
|
58
|
+
- app/helpers/universal_renderer/ssr_helpers.rb
|
59
|
+
- app/services/universal_renderer/static_client.rb
|
60
|
+
- app/services/universal_renderer/stream_client.rb
|
61
|
+
- app/services/universal_renderer/stream_client/error_logger.rb
|
62
|
+
- app/services/universal_renderer/stream_client/execution.rb
|
63
|
+
- app/services/universal_renderer/stream_client/setup.rb
|
64
|
+
- config/routes.rb
|
65
|
+
- lib/generators/universal_renderer/install_generator.rb
|
66
|
+
- lib/generators/universal_renderer/templates/initializer.rb
|
67
|
+
- lib/tasks/universal_renderer_tasks.rake
|
68
|
+
- lib/universal_renderer.rb
|
69
|
+
- lib/universal_renderer/configuration.rb
|
70
|
+
- lib/universal_renderer/engine.rb
|
71
|
+
- lib/universal_renderer/ssr_scrubber.rb
|
72
|
+
- lib/universal_renderer/version.rb
|
73
|
+
homepage: https://github.com/thaske/universal_renderer
|
74
|
+
licenses:
|
75
|
+
- MIT
|
76
|
+
metadata:
|
77
|
+
allowed_push_host: https://rubygems.org
|
78
|
+
homepage_uri: https://github.com/thaske/universal_renderer
|
79
|
+
changelog_uri: https://github.com/thaske/universal_renderer/blob/main/CHANGELOG.md
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 3.0.0
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubygems_version: 3.6.2
|
95
|
+
specification_version: 4
|
96
|
+
summary: Facilitates Server-Side Rendering (SSR) in Rails applications.
|
97
|
+
test_files: []
|