Commenti

Aggiungere target="blank" sui link esterni con un Rack middleware

Quante volte avete sentito, magari a termine dei lavori, la richiesta "tutti i link verso l'esterno dovrebbero aprirsi in un tab separato"? Questo è un tipico esempio di lavoro tremendamente noioso da fare per vie canoniche -- perchè richiederebbe un editing di tutti i link presenti in tutte le viste -- ma banale da realizzare passando per un middleware Rack.

In basso il codice. Il middleware usa Nokogiri per parsare tutte le pagine HTML (quelle con Content-Type impostato a text/html), e per ogni link trovato controlla il dominio: se non coincide con quello del server, aggiunge il fatidico attributo target al link.

target_blank.rb
 1 require 'nokogiri'
 2 
 3 module Rack
 4   class TargetBlank
 5     include Rack::Utils
 6 
 7     def initialize(app)
 8       @app = app
 9     end
10 
11     def call(env)
12       @request = Rack::Request.new(env)
13       status, @headers, @body = @app.call(env)
14       @headers = HeaderHash.new(@headers)
15       if is_html_content?
16         body = edit_external_links(body_to_string)
17         update_response_body(body)
18         update_content_length
19       end
20       [status, @headers, @body]
21     end
22 
23     private
24 
25     def edit_external_links(body)
26       doc = Nokogiri::HTML(body)
27       found_links = false
28       doc.css('a[href]').each do |link|
29         uri = URI(link['href'])
30         if uri.absolute? and uri.host != @request.host
31           link['target'] = 'blank'
32           found_links = true
33         end
34       end
35       found_links ? doc.to_html : body
36     end
37 
38     def body_to_string
39       s = ""
40       @body.each { |x| s << x }
41       s
42     end
43 
44     def update_content_length
45       length = 0
46       @body.each { |s| length += Rack::Utils.bytesize(s) }
47       @headers['Content-Length'] = length.to_s
48     end
49 
50     def update_response_body(body)
51       if @body.class.name == "ActionController::Response"
52         @body.body = body
53       else
54         @body = [body]
55       end
56     end
57 
58     def is_html_content?
59       @headers.key?('Content-Type') && @headers['Content-Type'].include?('text/html')
60     end
61   end
62 end