diff --git a/CHANGELOG.md b/CHANGELOG.md index b0a2c1c..a39f111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ ### Changelog -### 2.2.2 (Next) +### 2.3.0 (Next) +* [#183](https://github.com/slack-ruby/slack-ruby-bot-server/pull/183): Add a `config.server_pages` block to configure page middleware options centrally without changing the installed library's defaults - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent). * Your contribution here. ### 2.2.1 (2026/10/24) diff --git a/README.md b/README.md index 1dd0e72..a66f76f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Build a complete Slack bot service with Slack button integration, in Ruby. - [Extensions](#extensions) - [Service Class](#service-class) - [HTML Templates](#html-templates) + - [Page Middleware Configuration](#page-middleware-configuration) - [Access Tokens](#access-tokens) - [Sample Bots Using Slack Ruby Bot Server](#sample-bots-using-slack-ruby-bot-server) - [Copyright & License](#copyright--license) @@ -307,6 +308,25 @@ SlackRubyBotServer.configure do |config| end ``` +#### Page Middleware Configuration + +Use `config.server_pages` to configure the underlying [`Rack::ServerPages`](https://github.com/migrs/rack-server-pages) middleware in an initializer, before `SlackRubyBotServer::Api::Middleware.instance` is first accessed. The block receives its configuration object after the framework applies `view_paths`, so it can customize any option supported by your installed `rack-server-pages` version. + +For example, with `rack-server-pages` 0.3.0 or newer: + +```ruby +SlackRubyBotServer.configure do |config| + config.server_pages do |pages| + pages.cache_control = 'private, no-cache' + pages.static_cache_control = 'public, no-cache' + end +end +``` + +`cache_control` sets the default `Cache-Control` header for rendered templates, including generated CSS. `static_cache_control` sets it for static files. `no-cache` allows storage but requires revalidation before reuse; use `private, no-store` for sensitive pages. You can also configure `static_etags` and `static_timestamps` with version 0.3.0 or newer. These settings do not change downstream API responses. + +Without a block, the framework preserves the installed library's defaults and existing `view_paths` behavior. Registering another block replaces the previous one, and `SlackRubyBotServer::Config.reset!` clears it. The block runs when the middleware stack is built, not on each request. If you change configuration after initialization, rebuild the stack with `SlackRubyBotServer::Api::Middleware.reset!`; configure it before startup in production. + ### Access Tokens By default the implementation of [Team](lib/slack-ruby-bot-server/models/team) stores the value of the token with all the requested OAuth scopes in both `token` and `activated_user_access_token` (for backwards compatibility), along with `oauth_version` and `oauth_scope`. If a legacy Slack bot integration `bot_access_token` is present, it is stored as `token`, and `activated_user_access_token` is the token that has all the requested OAuth scopes. diff --git a/lib/slack-ruby-bot-server/api/middleware.rb b/lib/slack-ruby-bot-server/api/middleware.rb index bc4eac2..6808eed 100644 --- a/lib/slack-ruby-bot-server/api/middleware.rb +++ b/lib/slack-ruby-bot-server/api/middleware.rb @@ -30,6 +30,7 @@ def self.instance use Rack::ServerPages do |config| config.view_path = SlackRubyBotServer::Config.view_paths + SlackRubyBotServer::Config.server_pages&.call(config) end run Middleware.new diff --git a/lib/slack-ruby-bot-server/config.rb b/lib/slack-ruby-bot-server/config.rb index 4aadc34..9ee1599 100644 --- a/lib/slack-ruby-bot-server/config.rb +++ b/lib/slack-ruby-bot-server/config.rb @@ -14,6 +14,7 @@ def reset! self.service_class = SlackRubyBotServer::Service self.oauth_scope = nil self.oauth_version = :v2 + @server_pages = nil self.view_paths = [ 'views', @@ -56,6 +57,11 @@ def oauth_scope_s oauth_scope&.join(',') end + def server_pages(&block) + @server_pages = block if block + @server_pages + end + def activerecord? database_adapter == :activerecord end diff --git a/lib/slack-ruby-bot-server/version.rb b/lib/slack-ruby-bot-server/version.rb index c8fc792..acb00f4 100644 --- a/lib/slack-ruby-bot-server/version.rb +++ b/lib/slack-ruby-bot-server/version.rb @@ -1,3 +1,3 @@ module SlackRubyBotServer - VERSION = '2.2.2'.freeze + VERSION = '2.3.0'.freeze end diff --git a/spec/api/middleware_spec.rb b/spec/api/middleware_spec.rb index f28fb46..ee1a96c 100644 --- a/spec/api/middleware_spec.rb +++ b/spec/api/middleware_spec.rb @@ -1,6 +1,12 @@ require 'spec_helper' +require 'tmpdir' +require 'fileutils' describe SlackRubyBotServer::Api::Middleware do + before { described_class.reset! } + + after { described_class.reset! } + def middleware_classes(app) r = [app] @@ -13,11 +19,11 @@ def middleware_classes(app) context 'overriding view_paths' do before do - SlackRubyBotServer::Api::Middleware.reset! SlackRubyBotServer.configure do |config| config.view_paths << 'custom' end end + it 'uses custom view paths' do server_pages = middleware_classes(SlackRubyBotServer::Api::Middleware.instance)[-2] expect(server_pages).to be_a Rack::ServerPages @@ -26,4 +32,60 @@ def middleware_classes(app) expect(config[:view_path][-1]).to eq 'custom' end end + + context 'configuring server pages' do + let(:directory) { Dir.mktmpdir('bot-server-pages') } + let(:request) { Rack::MockRequest.new(described_class.instance) } + + before do + File.write(File.join(directory, 'index.html.erb'), '<%= "Rendered" %>') + SlackRubyBotServer.config.view_paths = [directory] + end + + after { FileUtils.remove_entry(directory) } + + it 'preserves default template caching when no block is configured' do + response = request.get('/') + expect(response.body).to eq 'Rendered' + expect(response['Cache-Control']).to be_nil + end + + it 'applies the block after the default view paths and only when building the stack' do + calls = [] + SlackRubyBotServer.configure do |config| + config.server_pages do |pages| + calls << pages.view_path + pages.cache_control = 'private, no-cache' + end + end + expect(request.get('/')['Cache-Control']).to eq 'private, no-cache' + request.get('/') + expect(calls).to eq [[directory]] + end + + it 'allows the block to override the framework view paths' do + SlackRubyBotServer.config.view_paths = [] + path = directory + SlackRubyBotServer.config.server_pages { |pages| pages.view_path = path } + expect(request.get('/').body).to eq 'Rendered' + end + + it 'replaces a previously registered block' do + SlackRubyBotServer.config.server_pages { |pages| pages.cache_control = 'public' } + SlackRubyBotServer.config.server_pages { |pages| pages.cache_control = 'private, no-store' } + expect(request.get('/')['Cache-Control']).to eq 'private, no-store' + end + + it 'clears the block when configuration is reset' do + SlackRubyBotServer.config.server_pages { |pages| pages.cache_control = 'private, no-store' } + SlackRubyBotServer.config.reset! + SlackRubyBotServer.config.view_paths = [directory] + expect(request.get('/')['Cache-Control']).to be_nil + end + + it 'does not hide configuration errors' do + SlackRubyBotServer.config.server_pages { raise ArgumentError, 'Invalid page configuration' } + expect { described_class.instance }.to raise_error ArgumentError, 'Invalid page configuration' + end + end end