meta_tags-rails 1.0.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.
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