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