rack-server-pages 0.2.1 → 0.3.0

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: b986555fd3422b75ae3b118c60d66db359f84298dd06cbc43fac84b036071fee
4
- data.tar.gz: 8e2a767d6ab041393514115fc1bc106d9b97b881ae4f061d47e99cae5cb6cdd2
3
+ metadata.gz: e901e5e22aabc19775de43f94a34d3ae8a8b27a4469db4563f9e78052350cc38
4
+ data.tar.gz: b853d1e26f38fd0ecb79c53f51415d5e907a30ce5c7ab5b9e92e2697f1539897
5
5
  SHA512:
6
- metadata.gz: 6e46b7346feca13de17285d305f54f2de30737eae130cf4e676e55349e63572340f6d30dd66fa0b18020926db5c91dece865b374e6dd165d605d78459924fbc4
7
- data.tar.gz: 9971d0027f89f8f47960c7ac0725af41288ef5c38b923bda5cfe74510bd627b7d5703ad9d9adc3379b76a3d09d5e2bb2a367014fb80d87b3d266dc29fab6bea3
6
+ metadata.gz: c6f387713227b1a9d7495d8763b6817305882999484fb0318d25fccd610781ff70222138d740a86f9cc3d716a55b336db3b33285e6ef9fdf24f27c88c56d91e8
7
+ data.tar.gz: 8fac86b85b2a9bb9aef795faf61036c2479ffe83171c4a3d2e63f6f096d19eb882f8f5128c51abccb1a08011fc3e31a6dfd087cd1a31dfadd6aaf74f7feaf4c7
data/CHANGES.md CHANGED
@@ -1,13 +1,26 @@
1
1
  CHANGES
2
2
  =======
3
3
 
4
- ### [v0.2.1](https://github.com/migrs/rack-server-pages/releases/tag/v0.2.1) / 2025-08-31
4
+ ### [v0.3.0](https://github.com/migrs/rack-server-pages/releases/tag/v0.3.0) / 2026-10-25
5
5
 
6
- * [Changes](https://github.com/migrs/rack-server-pages/compare/v0.2.0...v0.2.1)
6
+ * [Changes](https://github.com/migrs/rack-server-pages/compare/v0.2.1...v0.3.0)
7
7
 
8
8
  * Feature
9
+ - Add content-versioned asset URLs, `static_etags` and `static_timestamps` (both enabled by default), and configurable static cache headers. See [UPGRADING](UPGRADING.md) for caching changes.
10
+
11
+ * Bugfix
12
+ - Detect the loaded Tilt template API rather than an incomplete namespace created by another dependency.
9
13
 
10
14
  * Misc
15
+ - Enable line and branch coverage with SimpleCov and publish coverage to Coveralls from CI.
16
+ - Refresh the README, move its backlog into GitHub issues, and document rendering a template with `halt`.
17
+ - Modernize GitHub Actions and test Ruby 3.0 through 3.4 and 4.0 against Rack 2.2 and 3 with headless Firefox.
18
+
19
+ ### [v0.2.1](https://github.com/migrs/rack-server-pages/releases/tag/v0.2.1) / 2025-08-31
20
+
21
+ * [Changes](https://github.com/migrs/rack-server-pages/compare/v0.2.0...v0.2.1)
22
+
23
+ * Bugfix
11
24
  - Stop using `Rack::Logger`
12
25
 
13
26
  ### [v0.2.0](https://github.com/migrs/rack-server-pages/releases/tag/v0.2.0) / 2024-10-19
data/README.md CHANGED
@@ -1,18 +1,20 @@
1
1
  Rack Server Pages
2
2
  =================
3
3
 
4
- Rack middleware and application for serving dynamic pages in very simple way.
5
- There are no controllers or models, just only views like a jsp, asp and php!
4
+ Rack middleware and application for serving dynamic pages without controllers or models, using templates similar to JSP, ASP, and PHP. Requests map directly to files in your view directories. Built-in ERB works without extra runtime dependencies beyond Rack; Tilt and additional template engines are optional.
6
5
 
7
- <http://github.com/migrs/rack-server-pages>
6
+ <https://github.com/migrs/rack-server-pages>
8
7
 
9
8
  [![Build Status](https://github.com/migrs/rack-server-pages/actions/workflows/test.yml/badge.svg)](https://github.com/migrs/rack-server-pages/actions/workflows/test.yml)
10
- [![Gem Version](https://badge.fury.io/rb/rack-server-pages.svg)](http://badge.fury.io/rb/rack-server-pages)
9
+ [![Coverage Status](https://coveralls.io/repos/github/migrs/rack-server-pages/badge.svg?branch=master)](https://coveralls.io/github/migrs/rack-server-pages?branch=master)
10
+ [![Gem Version](https://badge.fury.io/rb/rack-server-pages.svg)](https://badge.fury.io/rb/rack-server-pages)
11
11
 
12
12
  ## Features
13
13
 
14
14
  - Serving dynamic pages (default: ERB)
15
15
  - Serving static files
16
+ - Content-versioned asset URLs with `asset_path`
17
+ - Configurable static-file caching with content-based `ETag` headers and timestamp validation
16
18
  - No requirements (except Rack)
17
19
  - Tilt support (optional)
18
20
  - Include a partial template
@@ -25,12 +27,16 @@ There are no controllers or models, just only views like a jsp, asp and php!
25
27
 
26
28
  ## Requirements
27
29
 
28
- - [Ruby](http://ruby-lang.org/) >= 2.2.2
29
- - [Rack](http://github.com/rack/rack)
30
+ - [Ruby](https://www.ruby-lang.org/)
31
+ - [Rack](https://github.com/rack/rack)
32
+
33
+ CI tests Ruby 3.0 through 3.4 and 4.0 against Rack 2.2 and 3, including browser integration tests with headless Firefox. Selenium manages the browser driver automatically.
34
+
35
+ The test suite collects line and branch coverage with SimpleCov before loading the library, including unexecuted files under `lib/`. Run `bundle exec rake spec` and open `coverage/index.html` to inspect the local report. CI publishes `coverage/.resultset.json` to Coveralls from the Ruby 4.0 / Rack 3 job only, so compatibility jobs do not overwrite each other's reports. Coverage output is ignored by Git.
30
36
 
31
37
  ## Install
32
38
 
33
- [RubyGems](http://rubygems.org/gems/rack-server-pages) available
39
+ Available on [RubyGems](https://rubygems.org/gems/rack-server-pages):
34
40
 
35
41
  ```
36
42
  gem install rack-server-pages
@@ -83,29 +89,30 @@ And same as above.
83
89
 
84
90
  ## Template bindings
85
91
 
86
- - CoreHelper
87
- - layout(file)
88
- - partial(file)
89
- - redirect(target, status=302) (same as [Sinatra](http://www.sinatrarb.com/intro#Browser%20Redirect))
90
- - halt(\*args) (same as [Sinatra](http://www.sinatrarb.com/intro#Halting))
91
- - url(path)
92
-
93
- - [Rack::Request](http://rack.rubyforge.org/doc/Rack/Request.html)
94
- - request
95
- - env
96
- - params
97
- - session
98
- - cookies
99
-
100
- - [Rack::Response](http://rack.rubyforge.org/doc/Rack/Response.html)
101
- - response
102
- - headers
103
- - set\_cookies
104
- - delete\_cookie
105
-
106
- - [ERB::Util](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/erb/rdoc/ERB/Util.html)
107
- - h (alias for: html\_escape)
108
- - u (alias for: url\_encode)
92
+ - `CoreHelper`
93
+ - `layout(file)`
94
+ - `partial(file)`
95
+ - `redirect(target, status=302)` (same as [Sinatra](http://www.sinatrarb.com/intro#Browser%20Redirect))
96
+ - `halt(*args)` (same as [Sinatra](http://www.sinatrarb.com/intro#Halting))
97
+ - `url(path)`
98
+ - `asset_path(path)`
99
+
100
+ - [`Rack::Request`](https://www.rubydoc.info/gems/rack/Rack/Request)
101
+ - `request`
102
+ - `env`
103
+ - `params`
104
+ - `session`
105
+ - `cookies`
106
+
107
+ - [`Rack::Response`](https://www.rubydoc.info/gems/rack/Rack/Response)
108
+ - `response`
109
+ - `headers`
110
+ - `set_cookies`
111
+ - `delete_cookie`
112
+
113
+ - [`ERB::Util`](https://docs.ruby-lang.org/en/master/ERB/Util.html)
114
+ - `h` (alias for: `html_escape`)
115
+ - `u` (alias for: `url_encode`)
109
116
 
110
117
  ## Configurations
111
118
 
@@ -143,21 +150,148 @@ run Rack::ServerPages.new { |config|
143
150
 
144
151
  ### Options
145
152
 
146
- - view\_path
147
- - Views folders to load templates from.
148
- - default: [views, public]
153
+ #### `view_path`
154
+
155
+ Sets the directories searched for templates and static files, in priority order. Accepts one directory or an array; the first matching file is used.
156
+
157
+ Default: `['views', 'public']`.
158
+
159
+ ```ruby
160
+ config.view_path = ['site/views', 'site/public']
161
+ ```
162
+
163
+ #### `effective_path`
164
+
165
+ Limits file serving to a URL prefix and removes that prefix when resolving files. Requests outside the prefix fall through to the downstream or fallback application.
166
+
167
+ Default: `nil` (no prefix).
168
+
169
+ ```ruby
170
+ config.effective_path = '/pages'
171
+ ```
172
+
173
+ This serves `/pages/about.html` from `about.html` under a configured `view_path` directory.
174
+
175
+ #### `default_charset`
176
+
177
+ Sets the default charset in rendered template responses' `Content-Type` header. Does not convert file contents or change static-file headers.
178
+
179
+ Default: `'utf-8'`.
180
+
181
+ ```ruby
182
+ config.default_charset = 'utf-8'
183
+ ```
184
+
185
+ #### `cache_control`
186
+
187
+ Sets the default `Cache-Control` response header for rendered templates, including generated CSS. A header explicitly set by a template or filter takes precedence. Static files use `static_cache_control` instead.
188
+
189
+ Default: `nil` (no policy added).
190
+
191
+ ```ruby
192
+ config.cache_control = 'private, no-store'
193
+ ```
194
+
195
+ This prevents caching pages containing sensitive information.
196
+
197
+ #### `static_cache_control`
198
+
199
+ Sets the `Cache-Control` response header for static files such as CSS, JavaScript, and images. Rendered templates, including generated CSS, use `cache_control` instead.
200
+
201
+ Default: `nil` (no policy added).
202
+
203
+ ```ruby
204
+ config.static_cache_control = 'public, no-cache'
205
+ ```
206
+
207
+ This allows caching but requires revalidation before reuse.
208
+
209
+ #### `static_etags`
210
+
211
+ Adds a content-based `ETag` response header to static files by hashing each file with SHA-256 on each request. A matching `If-None-Match` returns an empty `304` for `GET`/`HEAD` and takes precedence over `If-Modified-Since`.
212
+
213
+ Default: `true`.
214
+
215
+ ```ruby
216
+ config.static_etags = false
217
+ ```
218
+
219
+ This disables content hashing, `ETag` generation, and `ETag` validation without disabling `static_timestamps` or `asset_path`.
220
+
221
+ #### `static_timestamps`
222
+
223
+ Honors `If-Modified-Since` on static requests using Rack's timestamp-based conditional handling, unless enabled `static_etags` validation takes precedence.
224
+
225
+ Default: `true`.
226
+
227
+ ```ruby
228
+ config.static_timestamps = false
229
+ ```
230
+
231
+ This ignores `If-Modified-Since` when deployment timestamps are unreliable. The `Last-Modified` response header and `static_etags` validation are unaffected.
232
+
233
+ #### `failure_app`
234
+
235
+ Sets a fallback Rack application for requests that do not resolve to a template or static file. When used as middleware, the downstream application takes precedence over this setting.
236
+
237
+ Default: `nil` (uses the built-in `404` response when no downstream application is supplied).
149
238
 
150
- - effective\_path
151
- - default: nil
239
+ ```ruby
240
+ config.failure_app = ->(_env) { [404, { 'content-type' => 'text/plain' }, ['Page not found']] }
241
+ ```
242
+
243
+ #### `show_exceptions`
244
+
245
+ Controls whether template-rendering errors are raised to the caller or handled as a `500` response with `on_error` filters. Does not control a built-in error page.
246
+
247
+ Default: `nil` (raises when `RACK_ENV` is unset or `'development'`; handles errors in other environments).
248
+
249
+ ```ruby
250
+ config.show_exceptions = false
251
+ ```
252
+
253
+ This handles template errors instead of raising them.
254
+
255
+ ### Versioned assets and browser caching
256
+
257
+ Content-based `ETag` validation and versioned asset URLs work independently. With `static_etags = true` (the default), ordinary URLs such as `/styles.css` receive `ETag` headers and support `If-None-Match` without using `asset_path`. However, validators only help when the browser makes a request; they cannot prevent reuse of an asset that the browser considers fresh. `asset_path` changes the URL when the file contents change, so a browser fetching fresh HTML requests the new asset URL instead of reusing the old one. Use both for stronger stale-asset protection.
258
+
259
+ Use `asset_path` instead of a fixed local asset URL in templates:
260
+
261
+ ```erb
262
+ <link rel="stylesheet" href="<%= h asset_path('/styles.css') %>">
263
+ <script src="<%= h asset_path('/scripts/app.js') %>"></script>
264
+ <img src="<%= h asset_path('/img/logo.png') %>" alt="Logo">
265
+ ```
152
266
 
153
- - default\_charset
154
- - default: utf-8
267
+ The helper adds a SHA-256 content fingerprint, for example `/styles.css?v=...`. Changed file contents produce a new URL even when a deployment preserves file timestamps and sizes. Existing query parameters and fragments are preserved, and an existing `v` parameter is replaced. Paths are relative to the configured view paths; application mounts (`SCRIPT_NAME`) and `effective_path` are included automatically. Missing files raise `Errno::ENOENT`; external URLs and parent-directory traversal are rejected.
155
268
 
156
- - cache\_control
157
- - default: nil
269
+ Configure browser revalidation explicitly:
270
+
271
+ ```ruby
272
+ use Rack::ServerPages do |config|
273
+ config.cache_control = 'private, no-cache'
274
+ config.static_cache_control = 'public, no-cache'
275
+ end
276
+ ```
158
277
 
159
- - failure\_app
160
- - default: nil
278
+ Both settings default to `nil`, preserving the existing cache-policy defaults. `no-cache` permits storage but requires revalidation before reuse; use `private, no-store` instead for templates containing sensitive account or payment information. These settings do not change cache headers on the downstream application.
279
+
280
+ By default, static responses include content-based `ETag` headers, hashing the file on each request. Matching `If-None-Match` requests return an empty `304` response. When both validators are present, `If-None-Match` takes precedence over `If-Modified-Since`, so a changed file is returned for an old `ETag` even if its modification time is unchanged. Requests with only `If-Modified-Since` retain Rack's timestamp-based conditional handling.
281
+
282
+ Set `config.static_etags = false` to disable content hashing and content-based `ETag` validation. With the default timestamp-validation setting, this restores Rack's original static-file conditional handling. `static_cache_control` still applies, and `asset_path` remains available independently. See [UPGRADING](UPGRADING.md) when updating from an earlier release.
283
+
284
+ `static_timestamps` also defaults to `true`. Set `config.static_timestamps = false` to ignore timestamp-based request validation when deployment timestamps are unreliable. This does not remove the `Last-Modified` response header or disable `ETag` validation. Set both options to `false` to disable both conditional checks.
285
+
286
+ Use the helper for local images, fonts, icons, and other assets too. For URLs inside CSS, rename the stylesheet to `styles.css.erb` and render those references explicitly:
287
+
288
+ ```erb
289
+ .hero { background-image: url("<%= asset_path('/img/background.svg') %>"); }
290
+ ```
291
+
292
+ Continue linking to `/styles.css` through `asset_path`. The fingerprint identifies the selected source file; generated CSS must also use `cache_control = 'private, no-cache'` so changes to a referenced image are picked up even when the CSS template itself has not changed. Referenced files are not rewritten automatically, and no build pipeline is needed.
293
+
294
+ Do not set `immutable` or a long freshness lifetime for these query-versioned URLs: the middleware serves the current file, not an archive of old versions. This mechanism does not force an already-open page to reload; the new URLs take effect when it fetches fresh HTML.
161
295
 
162
296
  ### Helpers
163
297
 
@@ -173,7 +307,7 @@ use Rack::ServerPages do |config|
173
307
  end
174
308
  ```
175
309
 
176
- in view file (erb)
310
+ in view file (`.erb`)
177
311
 
178
312
  ```erb
179
313
  <%= three_times('blah') %>
@@ -211,7 +345,7 @@ end
211
345
 
212
346
  ### Filters
213
347
 
214
- with before/after block
348
+ with `before`/`after` block
215
349
 
216
350
  ```ruby
217
351
  use Rack::ServerPages do |config|
@@ -238,7 +372,7 @@ use Rack::ServerPages do |config|
238
372
  end
239
373
  ```
240
374
 
241
- if you define before/after method in helper module, it will be treated as filters
375
+ if you define a `before`/`after` method in a helper module, it will be treated as a filter
242
376
 
243
377
  ```ruby
244
378
  module SampleHelper
@@ -262,9 +396,26 @@ in view file
262
396
  <%= three_times(@title) %>
263
397
  ```
264
398
 
399
+ ### Rendering a template with `halt`
400
+
401
+ `partial` returns a rendered string, so pass its result to `halt` as the response body. For example, create `views/forbidden.html.erb`:
402
+
403
+ ```erb
404
+ <h1><%= 'Forbidden' %></h1>
405
+ ```
406
+
407
+ Inside a `before` filter, helper, or template, return that page with the desired status and content type:
408
+
409
+ ```ruby
410
+ halt 403, { Rack::CONTENT_TYPE => 'text/html; charset=utf-8' }, partial('views/forbidden.html')
411
+ ```
412
+
413
+ Place the call inside your existing validation or authorization check. `partial` takes a filesystem path relative to the working directory, or an absolute path; it does not search `view_path` automatically. Omitting the final `.erb` suffix lets it find the template. `halt` skips normal page rendering, so automatic layouts are not applied to this response. The example works with both built-in ERB and Tilt.
414
+
265
415
  ## Tilt support
266
- [Tilt](http://github.com/rtomayko/tilt) is generic interface to multiple Ruby template engines.
267
- If you want to use Tilt, just `require 'tilt'` and require template engine libraries that you want.
416
+ [Tilt](https://github.com/rtomayko/tilt) is a generic interface to multiple Ruby template engines. To use it, add Tilt and the desired engines to your dependencies, then explicitly require them:
417
+
418
+ Tilt is selected only when its template lookup API is available. A `Tilt` namespace created by another dependency does not enable it or force it to load. Loading Tilt later enables it for subsequent requests unless you have explicitly disabled it with `Rack::ServerPages::Template.use_tilt(false)`.
268
419
 
269
420
  ```ruby
270
421
  require 'rack-server-pages'
@@ -273,7 +424,12 @@ require 'rdiscount' # markdown library
273
424
  run Rack::ServerPages
274
425
  ```
275
426
 
276
- or put your `Gemfile`
427
+ If your application uses `Bundler.require`, the equivalent `Gemfile` entries are:
428
+
429
+ ```ruby
430
+ gem 'tilt', require: 'tilt'
431
+ gem 'rdiscount', require: 'rdiscount'
432
+ ```
277
433
 
278
434
  ### [Markdown](http://daringfireball.net/projects/markdown/)
279
435
 
@@ -468,21 +624,13 @@ At first, create sample file: `public/hello.erb` or `views/hello.html.erb`
468
624
 
469
625
  ### Rails
470
626
 
471
- Add to `config/environment.rb` (Rails2) or `config/application.rb` (Rails3)
627
+ Add the middleware inside your application configuration in `config/application.rb`:
472
628
 
473
629
  ```ruby
474
630
  config.middleware.use Rack::ServerPages
475
631
  ```
476
632
 
477
- And run
478
-
479
- Rails2
480
-
481
- ```
482
- script/server
483
- ```
484
-
485
- Rails3
633
+ Start Rails:
486
634
 
487
635
  ```
488
636
  rails s
@@ -520,15 +668,15 @@ ruby sinatra_sample.rb
520
668
 
521
669
  ### Customize file extension associations
522
670
 
523
- #### e.g. .php as ERB
671
+ #### e.g. `.php` as ERB
524
672
 
525
- ERBTemplate (default)
673
+ `ERBTemplate` (default)
526
674
 
527
675
  ```ruby
528
676
  Rack::ServerPages::Template::ERBTemplate.extensions << 'php'
529
677
  ```
530
678
 
531
- TiltTemplate (see. [Template Mappings](http://github.com/rtomayko/tilt))
679
+ `TiltTemplate` (see. [Template Mappings](http://github.com/rtomayko/tilt))
532
680
 
533
681
  ```ruby
534
682
  Tilt.register Tilt::ERBTemplate, 'php'
@@ -542,25 +690,22 @@ And create `public/info.php` :)
542
690
 
543
691
  <http://localhost:9292/info.php>
544
692
 
545
- ## Demo site
693
+ ## Development
694
+
695
+ Clone the repository and install its development dependencies. RedCloth and BlueCloth are legacy optional test engines; the C17 build options let them compile when Ruby defaults to C23:
546
696
 
547
- <http://rack-server-pages.heroku.com/>
697
+ ```sh
698
+ bundle config set --local build.RedCloth --with-cflags=-std=gnu17
699
+ bundle config set --local build.bluecloth --with-cflags=-std=gnu17
700
+ bundle install
701
+ MOZ_HEADLESS=1 bundle exec rake
702
+ ```
548
703
 
549
- ## ToDo
704
+ The default task runs lint and the full suite. Browser examples require Firefox; Selenium manages its driver. Run `bundle exec rake spec` for only the tests.
550
705
 
551
- ### Implements
552
- - Static file generator (for designer)
706
+ Run the included application with `bundle exec rackup` and visit <http://localhost:9292/> or <http://localhost:9292/examples/> for local examples. There is no maintained hosted demo.
553
707
 
554
- ### Tasks
555
- - Tutorials
556
- - for PHP user
557
- - for Designer
558
- - for Windows user
559
- - More documents
560
- - Deployment using apache / passenger / nginx
561
- - Complete Tilt examples
562
- - Philosophy
563
- - Benchmark
708
+ See [RELEASING](RELEASING.md) for the maintainer release process.
564
709
 
565
710
  ## License
566
711
 
data/RELEASING.md ADDED
@@ -0,0 +1,79 @@
1
+ # Releasing Rack Server Pages
2
+
3
+ Release bug fixes frequently, features less frequently, and breaking changes rarely.
4
+
5
+ ## Release
6
+
7
+ Start from a clean, up-to-date canonical checkout on `master`. Confirm that `origin` points to `migrs/rack-server-pages`, the version in `lib/rack-server-pages/version.rb` matches the next heading in [CHANGES.md](CHANGES.md), and the version has not already been tagged or published to RubyGems.
8
+
9
+ ```sh
10
+ git switch master
11
+ git pull --ff-only origin master
12
+ git status --short
13
+ ```
14
+
15
+ Run lint and the complete test suite with Firefox available. Selenium manages its driver; use headless mode locally. See [Development](README.md#development) for dependency setup.
16
+
17
+ ```sh
18
+ MOZ_HEADLESS=1 bundle exec rake
19
+ ```
20
+
21
+ Also run the suite against Rack 2.2. The `RACK_VERSION` selector must be set when resolving dependencies and running tests:
22
+
23
+ ```sh
24
+ RACK_VERSION='~> 2.2' bundle update rack rackup
25
+ RACK_VERSION='~> 2.2' MOZ_HEADLESS=1 bundle exec rake spec
26
+ ```
27
+
28
+ Return to the default Rack 3 dependencies afterward:
29
+
30
+ ```sh
31
+ bundle update rack rackup
32
+ ```
33
+
34
+ Review [UPGRADING.md](UPGRADING.md) for behavior changes. Replace `Next` in the release heading in `CHANGES.md` with the current date, preserving the existing `YYYY-MM-DD` format. Commit and push the release preparation:
35
+
36
+ ```sh
37
+ git add CHANGES.md
38
+ git commit -m "Preparing for release, 0.3.0."
39
+ git push origin master
40
+ ```
41
+
42
+ Wait for the release commit's [GitHub Actions](https://github.com/migrs/rack-server-pages/actions) checks to pass across the full Ruby/Rack matrix and lint. A successful build for a different commit is not sufficient.
43
+
44
+ Publishing requires a maintainer's RubyGems credentials and may require a fresh one-time password (OTP). For an agent-assisted release, request the OTP immediately before publishing, after validation and CI complete. Never commit or log credentials or OTPs. With `OTP` holding a freshly supplied code:
45
+
46
+ ```sh
47
+ GEM_HOST_OTP_CODE="$OTP" bundle exec rake release
48
+ unset OTP
49
+ ```
50
+
51
+ Bundler builds the gem in `pkg/`, creates the version tag, pushes the commit and tag, and publishes the gem. Verify that the remote tag points to the release commit and RubyGems lists the expected version. If a step fails, inspect what completed before retrying; never move a published release tag or overwrite a published version.
52
+
53
+ Create a GitHub release from the tag, using the corresponding changelog entry as release notes and linking to the upgrade guide for behavior changes:
54
+
55
+ ```sh
56
+ gh release create v0.3.0 --verify-tag --title "v0.3.0" --notes-file /path/to/release-notes.md
57
+ ```
58
+
59
+ ## Prepare the Next Version
60
+
61
+ Increment the patch version in `lib/rack-server-pages/version.rb`, then add a new `Next` section at the top of `CHANGES.md`, updating both its release and comparison links:
62
+
63
+ ```markdown
64
+ ### [v0.3.1](https://github.com/migrs/rack-server-pages/releases/tag/v0.3.1) / Next
65
+
66
+ * [Changes](https://github.com/migrs/rack-server-pages/compare/v0.3.0...v0.3.1)
67
+
68
+ * Feature
69
+
70
+ * Bugfix
71
+
72
+ * Misc
73
+ ```
74
+
75
+ ```sh
76
+ git add CHANGES.md lib/rack-server-pages/version.rb
77
+ git commit -m "Preparing for next development iteration, 0.3.1."
78
+ git push origin master
79
+ ```
data/UPGRADING.md ADDED
@@ -0,0 +1,23 @@
1
+ # Upgrading
2
+
3
+ ## 0.2.x to 0.3.0
4
+
5
+ ### Static-file caching changes by default
6
+
7
+ `static_etags` defaults to `true`. Static-file responses now include an ETag derived from a SHA-256 hash of the file contents. Matching `If-None-Match` validators return an empty `304` response for GET and HEAD. When both `If-None-Match` and `If-Modified-Since` are present, the ETag takes precedence: an old ETag produces a full response even if the file's timestamp is unchanged.
8
+
9
+ Requests with only `If-Modified-Since` retain Rack's timestamp-based conditional handling, including `304` responses for matching timestamps. Such requests still rely on accurate modification times; adopt content-versioned asset URLs for protection against deployment tools that preserve or normalize timestamps.
10
+
11
+ The file is read and hashed on each static request, adding disk and CPU work. To restore Rack's original static-file conditional handling without this content hashing, disable the option:
12
+
13
+ ```ruby
14
+ use Rack::ServerPages do |config|
15
+ config.static_etags = false
16
+ end
17
+ ```
18
+
19
+ This omits content-based ETags and, with the default timestamp-validation setting, delegates conditional handling entirely to Rack. `static_cache_control` still applies when configured. Template responses and downstream applications are unaffected by this option.
20
+
21
+ ### Optional timestamp-validation opt-out
22
+
23
+ `static_timestamps` defaults to `true`, preserving Rack's timestamp-only conditional responses. Set `config.static_timestamps = false` to ignore `If-Modified-Since` on static requests when modification times are unreliable. The `Last-Modified` response header is still emitted, and matching ETags can still produce a `304`. The switches are independent: set both `static_etags` and `static_timestamps` to `false` to disable both conditional checks. Neither option affects template responses or downstream applications.
@@ -4,6 +4,7 @@ require 'rack/utils'
4
4
  require 'rack/mime'
5
5
  require 'time'
6
6
  require 'forwardable'
7
+ require 'digest/sha2'
7
8
 
8
9
  module Rack
9
10
  class ServerPages
@@ -39,10 +40,49 @@ module Rack
39
40
  @app
40
41
  else
41
42
  file = select_template_file(files)
42
- (tpl = Template[file]) ? server_page(tpl) : Rack::Files.new(file.split(path_info).first)
43
+ (tpl = Template[file]) ? server_page(tpl) : static_file(file, path_info)
43
44
  end.call(env)
44
45
  end
45
46
 
47
+ module StaticFiles
48
+ def static_file(file, path_info)
49
+ root = file.split(path_info).first
50
+ return Rack::Files.new(root) unless ::File.file?(file) && ::File.readable?(file)
51
+
52
+ headers = static_headers(file)
53
+ files = Rack::Files.new(root, headers)
54
+ lambda do |env|
55
+ if @config.static_etags && %w[GET HEAD].include?(env['REQUEST_METHOD']) && matching_etag?(env['HTTP_IF_NONE_MATCH'], headers[Rack::ETAG])
56
+ [304, headers.dup, []]
57
+ else
58
+ files.call(static_request_env(env))
59
+ end
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def static_request_env(env)
66
+ # If-None-Match takes precedence over If-Modified-Since.
67
+ ignore_modified_since = !@config.static_timestamps || (@config.static_etags && env.key?('HTTP_IF_NONE_MATCH'))
68
+ ignore_modified_since ? env.merge('HTTP_IF_MODIFIED_SINCE' => nil) : env
69
+ end
70
+
71
+ def static_headers(file)
72
+ headers = @config.static_etags ? { Rack::ETAG => %("#{Digest::SHA256.file(file).hexdigest}") } : {}
73
+ @config.static_cache_control ? headers.merge(Rack::CACHE_CONTROL => @config.static_cache_control) : headers
74
+ end
75
+
76
+ def matching_etag?(condition, etag)
77
+ condition.to_s.split(',').any? do |candidate|
78
+ candidate = candidate.strip.sub(%r{\AW/}, '')
79
+ candidate == '*' || candidate == etag
80
+ end
81
+ end
82
+ end
83
+
84
+ include StaticFiles
85
+
46
86
  def find_template_files(path_info)
47
87
  if m = path_info.match(@path_regex)
48
88
  @config.view_paths.inject([]) do |files, path|
@@ -66,7 +106,7 @@ module Rack
66
106
 
67
107
  def server_page(template)
68
108
  lambda do |env|
69
- @binding.new(env).tap do |scope|
109
+ @binding.new(env, self).tap do |scope|
70
110
  catch(:halt) do
71
111
  begin
72
112
  @config.filter.invoke(scope, :before)
@@ -87,6 +127,36 @@ module Rack
87
127
  end
88
128
  end
89
129
 
130
+ module AssetPaths
131
+ def asset_path(path)
132
+ path, fragment = path.to_s.split('#', 2)
133
+ path, query = path.to_s.split('?', 2)
134
+ path, file = asset_file(path.to_s)
135
+ query = Rack::Utils.parse_query(query)
136
+ query['v'] = Digest::SHA256.file(file).hexdigest
137
+ "#{path}?#{Rack::Utils.build_query(query)}#{fragment && "##{fragment}"}"
138
+ end
139
+
140
+ private
141
+
142
+ def asset_file(path)
143
+ raise ArgumentError, 'asset_path requires a local asset path without parent-directory traversal' unless valid_asset_path?(CGI.unescape(path))
144
+
145
+ path = "#{@config.effective_path}/#{path.sub(%r{\A/}, '')}"
146
+ file = select_template_file(find_template_files(CGI.unescape(path)) || [])
147
+ raise Errno::ENOENT, path unless file && ::File.file?(file)
148
+
149
+ [path, file]
150
+ end
151
+
152
+ def valid_asset_path?(path)
153
+ !path.empty? && (path !~ %r{\A(?:[a-z][a-z0-9+.-]*:|//)}i) &&
154
+ !path.split('/').include?('..') && !path.include?("\0")
155
+ end
156
+ end
157
+
158
+ include AssetPaths
159
+
90
160
  class Filter
91
161
  TYPES = [:before, :after, :on_error].freeze
92
162
  TYPES.each do |type|
@@ -143,7 +213,8 @@ module Rack
143
213
  end
144
214
  end
145
215
 
146
- hash_accessor :view_path, :effective_path, :cache_control, :default_charset, :failure_app, :show_exceptions
216
+ hash_accessor :view_path, :effective_path, :cache_control, :static_cache_control, :static_etags, :static_timestamps,
217
+ :default_charset, :failure_app, :show_exceptions
147
218
 
148
219
  attr_reader :filter
149
220
 
@@ -153,6 +224,8 @@ module Rack
153
224
  super
154
225
  self[:default_charset] ||= 'utf-8'
155
226
  self[:view_path] ||= %w(views public)
227
+ self[:static_etags] = true
228
+ self[:static_timestamps] = true
156
229
  @helpers = []
157
230
  @filter = Filter.new
158
231
  end
@@ -187,7 +260,7 @@ module Rack
187
260
  end
188
261
 
189
262
  def tilt?
190
- @use_tilt.nil? ? (@use_tilt ||= !!defined?(Tilt)) : @use_tilt && defined?(Tilt)
263
+ @use_tilt != false && !!defined?(Tilt) && Tilt.respond_to?(:[])
191
264
  end
192
265
 
193
266
  def use_tilt(bool = true)
@@ -273,6 +346,10 @@ module Rack
273
346
  end
274
347
 
275
348
  module CoreHelper
349
+ def asset_path(path)
350
+ url(@server_pages.asset_path(path))
351
+ end
352
+
276
353
  def redirect(target, status = 302)
277
354
  response.redirect(target, status)
278
355
  halt
@@ -329,9 +406,10 @@ module Rack
329
406
  end
330
407
  end
331
408
 
332
- def initialize(env)
409
+ def initialize(env, server_pages = nil)
333
410
  @request = Rack::Request.new(env)
334
411
  @response = Rack::Response.new
412
+ @server_pages = server_pages
335
413
  # @response['Content-Type'] = nil
336
414
  end
337
415
 
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  class ServerPages
3
- VERSION = '0.2.1'.freeze
3
+ VERSION = '0.3.0'.freeze
4
4
  end
5
5
  end
@@ -12,7 +12,7 @@ Gem::Specification.new do |s|
12
12
  s.summary = 'Rack middleware and appilcation for serving dynamic pages.'
13
13
  s.description = 'Rack middleware and appilcation for serving dynamic pages in very simple way, without controllers or models, only views similar to ASP, JSP and PHP.'
14
14
 
15
- s.files = Dir['lib/**/*', 'CHANGES.md', 'README.md', 'LICENSE.md', 'rack-server-pages.gemspec']
15
+ s.files = Dir['lib/**/*', 'CHANGES.md', 'README.md', 'UPGRADING.md', 'RELEASING.md', 'LICENSE.md', 'rack-server-pages.gemspec']
16
16
 
17
17
  s.require_paths = ['lib']
18
18
 
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-server-pages
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masato Igarashi
8
8
  - Daniel Doubrovkine
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2025-08-31 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: rack
@@ -36,6 +35,8 @@ files:
36
35
  - CHANGES.md
37
36
  - LICENSE.md
38
37
  - README.md
38
+ - RELEASING.md
39
+ - UPGRADING.md
39
40
  - lib/rack-server-pages.rb
40
41
  - lib/rack-server-pages/version.rb
41
42
  - lib/rack/server_pages.rb
@@ -44,7 +45,6 @@ homepage: http://github.com/migrs/rack-server-pages
44
45
  licenses:
45
46
  - MIT
46
47
  metadata: {}
47
- post_install_message:
48
48
  rdoc_options: []
49
49
  require_paths:
50
50
  - lib
@@ -59,8 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
59
59
  - !ruby/object:Gem::Version
60
60
  version: '0'
61
61
  requirements: []
62
- rubygems_version: 3.5.16
63
- signing_key:
62
+ rubygems_version: 4.0.20
64
63
  specification_version: 4
65
64
  summary: Rack middleware and appilcation for serving dynamic pages.
66
65
  test_files: []