meta_tags-rails 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.rspec +3 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +21 -0
  7. data/CHANGELOG.md +135 -0
  8. data/Gemfile +13 -0
  9. data/MIT-LICENSE +22 -0
  10. data/README.md +627 -0
  11. data/Rakefile +28 -0
  12. data/lib/meta_tags-rails.rb +37 -0
  13. data/lib/meta_tags-rails/configuration.rb +21 -0
  14. data/lib/meta_tags-rails/content_tag.rb +14 -0
  15. data/lib/meta_tags-rails/controller_helper.rb +44 -0
  16. data/lib/meta_tags-rails/meta_tags_collection.rb +176 -0
  17. data/lib/meta_tags-rails/renderer.rb +253 -0
  18. data/lib/meta_tags-rails/tag.rb +25 -0
  19. data/lib/meta_tags-rails/text_normalizer.rb +148 -0
  20. data/lib/meta_tags-rails/version.rb +4 -0
  21. data/lib/meta_tags-rails/view_helper.rb +205 -0
  22. data/meta-tags.gemspec +30 -0
  23. data/spec/configuration_spec.rb +14 -0
  24. data/spec/controller_helper_spec.rb +42 -0
  25. data/spec/spec_helper.rb +84 -0
  26. data/spec/text_normalizer/normalize_title_spec.rb +43 -0
  27. data/spec/text_normalizer/truncate_array_spec.rb +60 -0
  28. data/spec/view_helper/charset_spec.rb +16 -0
  29. data/spec/view_helper/custom_spec.rb +67 -0
  30. data/spec/view_helper/description_spec.rb +61 -0
  31. data/spec/view_helper/icon_spec.rb +42 -0
  32. data/spec/view_helper/keywords_spec.rb +58 -0
  33. data/spec/view_helper/links_spec.rb +125 -0
  34. data/spec/view_helper/module_spec.rb +41 -0
  35. data/spec/view_helper/noindex_spec.rb +107 -0
  36. data/spec/view_helper/open_graph_spec.rb +86 -0
  37. data/spec/view_helper/open_search_spec.rb +33 -0
  38. data/spec/view_helper/refresh_spec.rb +32 -0
  39. data/spec/view_helper/title_spec.rb +155 -0
  40. data/spec/view_helper/twitter_spec.rb +31 -0
  41. data/spec/view_helper_spec.rb +57 -0
  42. metadata +172 -0
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task test: :spec
8
+ task default: :spec
9
+
10
+ desc 'Starts irb with MetaTags gem loaded'
11
+ task :console do
12
+ require 'irb'
13
+
14
+ $:.unshift File.expand_path('../lib', __FILE__)
15
+ require 'meta_tags'
16
+ ARGV.clear
17
+ IRB.start
18
+ end
19
+
20
+ require 'yard'
21
+ YARD::Rake::YardocTask.new(:yard) do |t|
22
+ t.options = ['--title', 'MetaTags Documentation']
23
+ if ENV['PRIVATE']
24
+ t.options.concat ['--protected', '--private']
25
+ else
26
+ t.options.concat ['--protected', '--no-private']
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ require 'action_controller'
2
+ require 'action_view'
3
+
4
+ # MetaTags gem namespace.
5
+ module MetaTags
6
+ # Returns MetaTags gem configuration.
7
+ #
8
+ def self.config
9
+ @@config ||= Configuration.new
10
+ end
11
+
12
+ # Configures MetaTags gem.
13
+ #
14
+ # @yield [Configuration] configuration object.
15
+ # @example
16
+ #
17
+ # MetaTags.configure do |config|
18
+ # # config.title_limit = 100
19
+ # end
20
+ def self.configure
21
+ yield config
22
+ end
23
+ end
24
+
25
+ require 'meta_tags-rails/version'
26
+
27
+ require 'meta_tags-rails/configuration'
28
+ require 'meta_tags-rails/controller_helper'
29
+ require 'meta_tags-rails/meta_tags_collection'
30
+ require 'meta_tags-rails/renderer'
31
+ require 'meta_tags-rails/tag'
32
+ require 'meta_tags-rails/content_tag'
33
+ require 'meta_tags-rails/text_normalizer'
34
+ require 'meta_tags-rails/view_helper'
35
+
36
+ ActionView::Base.send :include, MetaTags::ViewHelper
37
+ ActionController::Base.send :include, MetaTags::ControllerHelper
@@ -0,0 +1,21 @@
1
+ module MetaTags
2
+ # MetaTags configuration.
3
+ class Configuration
4
+ # How many characters to truncate title to.
5
+ attr_accessor :title_limit
6
+ # How many characters to truncate description to.
7
+ attr_accessor :description_limit
8
+ # How many characters to truncate keywords to.
9
+ attr_accessor :keywords_limit
10
+ # Keywords separator - a string to join keywords with.
11
+ attr_accessor :keywords_separator
12
+
13
+ # Initializes a new instance of Configuration class.
14
+ def initialize
15
+ @title_limit = 70
16
+ @description_limit = 160
17
+ @keywords_limit = 255
18
+ @keywords_separator = ', '
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ module MetaTags
2
+ # Represents an HTML meta tag with content (<tag></tag>).
3
+ # Content should be passed as a `:content` attribute.
4
+ class ContentTag < Tag
5
+ # Render tag into a Rails view.
6
+ #
7
+ # @param [ActionView::Base] view instance of a Rails view.
8
+ # @return [String] HTML string for the tag.
9
+ #
10
+ def render(view)
11
+ view.content_tag(name, attributes[:content], attributes.except(:content))
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,44 @@
1
+ module MetaTags
2
+ # Contains methods to use in controllers.
3
+ #
4
+ # You can define several instance variables to set meta tags:
5
+ # @page_title = 'Member Login'
6
+ # @page_description = 'Member login page.'
7
+ # @page_keywords = 'Site, Login, Members'
8
+ #
9
+ # Also you can use {#set_meta_tags} method, that have the same parameters
10
+ # as {ViewHelper#set_meta_tags}.
11
+ #
12
+ module ControllerHelper
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ alias_method_chain :render, :meta_tags
17
+ end
18
+
19
+ # Processes the <tt>@page_title</tt>, <tt>@page_keywords</tt>, and
20
+ # <tt>@page_description</tt> instance variables and calls +render+.
21
+ def render_with_meta_tags(*args, &block)
22
+ self.meta_tags[:title] = @page_title if @page_title
23
+ self.meta_tags[:keywords] = @page_keywords if @page_keywords
24
+ self.meta_tags[:description] = @page_description if @page_description
25
+
26
+ render_without_meta_tags(*args, &block)
27
+ end
28
+ protected :render_with_meta_tags
29
+
30
+ # Set meta tags for the page.
31
+ #
32
+ # See <tt>MetaTags::ViewHelper#set_meta_tags</tt> for details.
33
+ def set_meta_tags(meta_tags)
34
+ self.meta_tags.update(meta_tags)
35
+ end
36
+ protected :set_meta_tags
37
+
38
+ # Get meta tags for the page.
39
+ def meta_tags
40
+ @meta_tags ||= MetaTagsCollection.new
41
+ end
42
+ protected :meta_tags
43
+ end
44
+ end
@@ -0,0 +1,176 @@
1
+ module MetaTags
2
+ # Class represents a collection of meta tags. Basically a wrapper around
3
+ # HashWithIndifferentAccess, with some additional helper methods.
4
+ class MetaTagsCollection
5
+ attr_reader :meta_tags
6
+
7
+ # Initializes a new instance of MetaTagsCollection.
8
+ #
9
+ def initialize
10
+ @meta_tags = HashWithIndifferentAccess.new
11
+ end
12
+
13
+ # Returns meta tag value by name.
14
+ #
15
+ # @param [String, Symbol] name meta tag name.
16
+ # @return meta tag value.
17
+ #
18
+ def [](name)
19
+ @meta_tags[name]
20
+ end
21
+
22
+ # Sets meta tag value by name.
23
+ #
24
+ # @param [String, Symbol] name meta tag name.
25
+ # @param value meta tag value.
26
+ # @return meta tag value.
27
+ #
28
+ def []=(name, value)
29
+ @meta_tags[name] = value
30
+ end
31
+
32
+ # Recursively merges a Hash of meta tag attributes into current list.
33
+ #
34
+ # @param [Hash] meta_tags meta tags Hash to merge into current list.
35
+ # @return [Hash] result of the merge.
36
+ #
37
+ def update(meta_tags = {})
38
+ @meta_tags.deep_merge! normalize_open_graph(meta_tags)
39
+ end
40
+
41
+ # Temporary merges defaults with current meta tags list and yields the block.
42
+ #
43
+ # @param [Hash] defaults list of default meta tag attributes.
44
+ # @return result of the block call.
45
+ #
46
+ def with_defaults(defaults = {})
47
+ old_meta_tags = @meta_tags
48
+ @meta_tags = normalize_open_graph(defaults).deep_merge!(self.meta_tags)
49
+ yield
50
+ ensure
51
+ @meta_tags = old_meta_tags
52
+ end
53
+
54
+ # Constructs the full title as if it would be rendered in title meta tag.
55
+ #
56
+ # @param [Hash] defaults list of default meta tag attributes.
57
+ # @return [String] page title.
58
+ #
59
+ def full_title(defaults = {})
60
+ with_defaults(defaults) { extract_full_title }
61
+ end
62
+
63
+ # Deletes and returns a meta tag value by name.
64
+ #
65
+ # @param [String, Symbol] name meta tag name.
66
+ # @return [Object] meta tag value.
67
+ #
68
+ def extract(name)
69
+ @meta_tags.delete(name)
70
+ end
71
+
72
+ # Deletes specified meta tags.
73
+ #
74
+ # @param [Array<String, Symbol>] names a list of meta tags to delete.
75
+ #
76
+ def delete(*names)
77
+ names.each { |name| @meta_tags.delete(name) }
78
+ end
79
+
80
+ # Extracts full page title and deletes all related meta tags.
81
+ #
82
+ # @return [String] page title.
83
+ #
84
+ def extract_full_title
85
+ site_title = extract(:site) || ''
86
+ title = extract_title || []
87
+ separator = extract_separator
88
+ reverse = extract(:reverse) === true
89
+
90
+ TextNormalizer.normalize_title(site_title, title, separator, reverse)
91
+ end
92
+
93
+ # Extracts page title as an array of segments without site title and separators.
94
+ #
95
+ # @return [Array<String>] segments of page title.
96
+ #
97
+ def extract_title
98
+ title = extract(:title).presence
99
+ return unless title
100
+
101
+ title = Array(title)
102
+ title.each(&:downcase!) if extract(:lowercase) === true
103
+ title
104
+ end
105
+
106
+ # Extracts title separator as a string.
107
+ #
108
+ # @return [String] page title separator.
109
+ #
110
+ def extract_separator
111
+ if meta_tags[:separator] === false
112
+ # Special case: if separator is hidden, do not display suffix/prefix
113
+ prefix = separator = suffix = ''
114
+ else
115
+ prefix = extract_separator_section(:prefix, ' ')
116
+ separator = extract_separator_section(:separator, '|')
117
+ suffix = extract_separator_section(:suffix, ' ')
118
+ end
119
+ delete(:separator, :prefix, :suffix)
120
+
121
+ TextNormalizer.safe_join([prefix, separator, suffix], '')
122
+ end
123
+
124
+ # Extracts noindex settings as a Hash mapping noindex tag name to value.
125
+ #
126
+ # @return [Hash<String,String>] noindex attributes.
127
+ #
128
+ def extract_noindex
129
+ noindex_name, noindex_value = extract_noindex_attribute(:noindex)
130
+ nofollow_name, nofollow_value = extract_noindex_attribute(:nofollow)
131
+
132
+ if noindex_name == nofollow_name
133
+ { noindex_name => [noindex_value, nofollow_value].compact.join(', ') }
134
+ else
135
+ { noindex_name => noindex_value, nofollow_name => nofollow_value }
136
+ end
137
+ end
138
+
139
+ protected
140
+
141
+ # Converts input hash to HashWithIndifferentAccess and renames :open_graph to :og.
142
+ #
143
+ # @param [Hash] meta_tags list of meta tags.
144
+ # @return [HashWithIndifferentAccess] normalized meta tags list.
145
+ #
146
+ def normalize_open_graph(meta_tags)
147
+ meta_tags = meta_tags.is_a?(HashWithIndifferentAccess) ? meta_tags.dup : meta_tags.with_indifferent_access
148
+ meta_tags[:og] = meta_tags.delete(:open_graph) if meta_tags.key?(:open_graph)
149
+ meta_tags
150
+ end
151
+
152
+ # Extracts separator segment without deleting it from meta tags list.
153
+ # If the value is false, empty string will be returned.
154
+ #
155
+ # @param [Symbol, String] name separator segment name.
156
+ # @param [String] default default value.
157
+ # @return [String] separator segment value.
158
+ #
159
+ def extract_separator_section(name, default)
160
+ meta_tags[name] === false ? '' : (meta_tags[name] || default)
161
+ end
162
+
163
+ # Extracts noindex attribute name and value without deleting it from meta tags list.
164
+ #
165
+ # @param [String, Symbol] name noindex attribute name.
166
+ # @return [Array<String>] pair of noindex attribute name and value.
167
+ #
168
+ def extract_noindex_attribute(name)
169
+ noindex = extract(name)
170
+ noindex_name = String === noindex ? noindex : 'robots'
171
+ noindex_value = noindex ? name.to_s : nil
172
+
173
+ [ noindex_name, noindex_value ]
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,253 @@
1
+ module MetaTags
2
+ # This class is used by MetaTags gems to render HTML meta tags into page.
3
+ class Renderer
4
+ attr_reader :meta_tags, :normalized_meta_tags
5
+
6
+ # Initialized a new instance of Renderer.
7
+ #
8
+ # @param [MetaTagsCollection] meta_tags meta tags object to render.
9
+ #
10
+ def initialize(meta_tags)
11
+ @meta_tags = meta_tags
12
+ @normalized_meta_tags = {}
13
+ end
14
+
15
+ # Renders meta tags on the page.
16
+ #
17
+ # @param [ActionView::Base] view Rails view object.
18
+ def render(view)
19
+ tags = []
20
+
21
+ render_charset(tags)
22
+ render_title(tags)
23
+ render_icon(tags)
24
+ render_with_normalization(tags, :description)
25
+ render_with_normalization(tags, :keywords)
26
+ render_refresh(tags)
27
+ render_noindex(tags)
28
+ render_alternate(tags)
29
+ render_open_search(tags)
30
+ render_links(tags)
31
+
32
+ render_hash(tags, :og, name_key: :property)
33
+ render_hashes(tags)
34
+ render_custom(tags)
35
+
36
+ tags.compact.map { |tag| tag.render(view) }.join("\n").html_safe
37
+ end
38
+
39
+ protected
40
+
41
+
42
+ # Renders charset tag.
43
+ #
44
+ # @param [Array<Tag>] tags a buffer object to store tag in.
45
+ #
46
+ def render_charset(tags)
47
+ if charset = meta_tags.extract(:charset)
48
+ tags << Tag.new(:meta, charset: charset) if charset.present?
49
+ end
50
+ end
51
+
52
+ # Renders title tag.
53
+ #
54
+ # @param [Array<Tag>] tags a buffer object to store tag in.
55
+ #
56
+ def render_title(tags)
57
+ title = meta_tags.extract_full_title
58
+ normalized_meta_tags[:title] = title
59
+ tags << ContentTag.new(:title, content: title) if title.present?
60
+ end
61
+
62
+ # Renders icon(s) tag.
63
+ #
64
+ # @param [Array<Tag>] tags a buffer object to store tag in.
65
+ #
66
+ def render_icon(tags)
67
+ if icon = meta_tags.extract(:icon)
68
+ if String === icon
69
+ tags << Tag.new(:link, rel: 'icon', href: icon, type: 'image/x-icon')
70
+ else
71
+ icon = [icon] if Hash === icon
72
+ icon.each do |icon_params|
73
+ icon_params = { rel: 'icon', type: 'image/x-icon' }.with_indifferent_access.merge(icon_params)
74
+ tags << Tag.new(:link, icon_params)
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ # Renders meta tag with normalization (should have a corresponding normalize_
81
+ # method in TextNormalizer).
82
+ #
83
+ # @param [Array<Tag>] tags a buffer object to store tag in.
84
+ # @see TextNormalizer
85
+ #
86
+ def render_with_normalization(tags, name)
87
+ value = TextNormalizer.public_send("normalize_#{name}", meta_tags.extract(name))
88
+ normalized_meta_tags[name] = value
89
+ tags << Tag.new(:meta, name: name, content: value) if value.present?
90
+ end
91
+
92
+ # Renders noindex and nofollow meta tags.
93
+ #
94
+ # @param [Array<Tag>] tags a buffer object to store tag in.
95
+ #
96
+ def render_noindex(tags)
97
+ meta_tags.extract_noindex.each do |name, content|
98
+ tags << Tag.new(:meta, name: name, content: content) if content.present?
99
+ end
100
+ end
101
+
102
+ # Renders refresh meta tag.
103
+ #
104
+ # @param [Array<Tag>] tags a buffer object to store tag in.
105
+ #
106
+ def render_refresh(tags)
107
+ if refresh = meta_tags.extract(:refresh)
108
+ tags << Tag.new(:meta, 'http-equiv' => 'refresh', content: refresh.to_s) if refresh.present?
109
+ end
110
+ end
111
+
112
+ # Renders alternate link tags.
113
+ #
114
+ # @param [Array<Tag>] tags a buffer object to store tag in.
115
+ #
116
+ def render_alternate(tags)
117
+ if alternate = meta_tags.extract(:alternate)
118
+ if Hash === alternate
119
+ alternate.each do |hreflang, href|
120
+ tags << Tag.new(:link, rel: 'alternate', href: href, hreflang: hreflang) if href.present?
121
+ end
122
+ elsif Array === alternate
123
+ alternate.each do |link_params|
124
+ tags << Tag.new(:link, { rel: 'alternate' }.with_indifferent_access.merge(link_params))
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ # Renders open_search link tag.
131
+ #
132
+ # @param [Array<Tag>] tags a buffer object to store tag in.
133
+ #
134
+ def render_open_search(tags)
135
+ if open_search = meta_tags.extract(:open_search)
136
+ href = open_search[:href]
137
+ title = open_search[:title]
138
+ if href.present?
139
+ type = "application/opensearchdescription+xml"
140
+ tags << Tag.new(:link, rel: 'search', type: type, href: href, title: title)
141
+ end
142
+ end
143
+ end
144
+
145
+ # Renders links.
146
+ #
147
+ # @param [Array<Tag>] tags a buffer object to store tag in.
148
+ #
149
+ def render_links(tags)
150
+ [ :canonical, :prev, :next, :author, :publisher ].each do |tag_name|
151
+ href = meta_tags.extract(tag_name)
152
+ if href.present?
153
+ @normalized_meta_tags[tag_name] = href
154
+ tags << Tag.new(:link, rel: tag_name, href: href)
155
+ end
156
+ end
157
+ end
158
+
159
+ # Renders complex hash objects.
160
+ #
161
+ # @param [Array<Tag>] tags a buffer object to store tag in.
162
+ #
163
+ def render_hashes(tags, options = {})
164
+ meta_tags.meta_tags.each do |property, data|
165
+ if data.is_a?(Hash)
166
+ process_hash(tags, property, data, options)
167
+ meta_tags.extract(property)
168
+ end
169
+ end
170
+ end
171
+
172
+ # Renders a complex hash object by key.
173
+ #
174
+ # @param [Array<Tag>] tags a buffer object to store tag in.
175
+ #
176
+ def render_hash(tags, key, options = {})
177
+ data = meta_tags.meta_tags[key]
178
+ if data.is_a?(Hash)
179
+ process_hash(tags, key, data, options)
180
+ meta_tags.extract(key)
181
+ end
182
+ end
183
+
184
+ # Renders custom meta tags.
185
+ #
186
+ # @param [Array<Tag>] tags a buffer object to store tag in.
187
+ #
188
+ def render_custom(tags)
189
+ meta_tags.meta_tags.each do |name, data|
190
+ Array(data).each do |val|
191
+ tags << Tag.new(:meta, name: name, content: val)
192
+ end
193
+ meta_tags.extract(name)
194
+ end
195
+ end
196
+
197
+ # Recursive method to process all the hashes and arrays on meta tags
198
+ #
199
+ # @param [Array<Tag>] tags a buffer object to store tag in.
200
+ # @param [String, Symbol] property a Hash or a String to render as meta tag.
201
+ # @param [Hash, Array, String, Symbol] content text content or a symbol reference to
202
+ # top-level meta tag.
203
+ #
204
+ def process_tree(tags, property, content, options = {})
205
+ method = case content
206
+ when Hash
207
+ :process_hash
208
+ when Array
209
+ :process_array
210
+ else
211
+ :render_tag
212
+ end
213
+ send(method, tags, property, content, options)
214
+ end
215
+
216
+ # Recursive method to process a hash with meta tags
217
+ #
218
+ # @param [Array<Tag>] tags a buffer object to store tag in.
219
+ # @param [String, Symbol] property a Hash or a String to render as meta tag.
220
+ # @param [Hash] content nested meta tag attributes.
221
+ #
222
+ def process_hash(tags, property, content, options = {})
223
+ content.each do |key, value|
224
+ key = key.to_s == '_' ? property : "#{property}:#{key}"
225
+ value = normalized_meta_tags[value] if value.is_a?(Symbol)
226
+ process_tree(tags, key, value, options)
227
+ end
228
+ end
229
+
230
+ # Recursive method to process a hash with meta tags
231
+ #
232
+ # @param [Array<Tag>] tags a buffer object to store tag in.
233
+ # @param [String, Symbol] property a Hash or a String to render as meta tag.
234
+ # @param [Array] content array of nested meta tag attributes or values.
235
+ #
236
+ def process_array(tags, property, content, options = {})
237
+ content.each { |v| process_tree(tags, property, v, options) }
238
+ end
239
+
240
+ # Recursive method to process a hash with meta tags
241
+ #
242
+ # @param [Array<Tag>] tags a buffer object to store tag in.
243
+ # @param [String, Symbol] name a Hash or a String to render as meta tag.
244
+ # @param [String, Symbol] value text content or a symbol reference to
245
+ # top-level meta tag.
246
+ #
247
+ def render_tag(tags, name, value, options = {})
248
+ name_key = options.fetch(:name_key, :name)
249
+ value_key = options.fetch(:value_key, :content)
250
+ tags << Tag.new(:meta, name_key => name.to_s, value_key => value) unless value.blank?
251
+ end
252
+ end
253
+ end