para-seo_tools 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +178 -0
- data/Rakefile +2 -0
- data/app/.DS_Store +0 -0
- data/app/components/seo_tools_skeleton_component.rb +7 -0
- data/app/controllers/.DS_Store +0 -0
- data/app/controllers/admin/para/seo_tools/skeleton_resources_controller.rb +21 -0
- data/app/decorators/seo_tools_skeleton_component_decorator.rb +3 -0
- data/app/helpers/para/seo_tools/admin/fields_helper.rb +15 -0
- data/app/models/para/seo_tools/page.rb +54 -0
- data/app/views/.DS_Store +0 -0
- data/app/views/admin/.DS_Store +0 -0
- data/app/views/admin/para/seo_tools/pages/_actions.html.haml +5 -0
- data/app/views/admin/para/seo_tools/pages/_filters.html.haml +25 -0
- data/app/views/admin/para/seo_tools/pages/_form.html.haml +13 -0
- data/app/views/admin/para/seo_tools/pages/_table.html.haml +20 -0
- data/app/views/admin/para/seo_tools/skeleton_resources/index.html.haml +4 -0
- data/bin/rails +12 -0
- data/config/locales/para-seo_tools.fr.yml +22 -0
- data/db/migrate/20150601085253_create_seo_tools_pages.rb +14 -0
- data/db/migrate/20160610094032_add_meta_tags_to_seo_tools_pages.rb +12 -0
- data/db/migrate/20160614081242_add_locale_and_remove_meta_tags_list_to_seo_tools_pages.rb +8 -0
- data/lib/generators/para/seo_tools/install/install_generator.rb +24 -0
- data/lib/generators/para/seo_tools/install/templates/initializer.rb +33 -0
- data/lib/generators/para/seo_tools/install/templates/skeleton.rb +37 -0
- data/lib/para/seo_tools/controller.rb +50 -0
- data/lib/para/seo_tools/engine.rb +27 -0
- data/lib/para/seo_tools/meta_tags/renderer.rb +80 -0
- data/lib/para/seo_tools/meta_tags/store.rb +52 -0
- data/lib/para/seo_tools/meta_tags/tags/base.rb +49 -0
- data/lib/para/seo_tools/meta_tags/tags/description.rb +54 -0
- data/lib/para/seo_tools/meta_tags/tags/image.rb +46 -0
- data/lib/para/seo_tools/meta_tags/tags/keywords.rb +20 -0
- data/lib/para/seo_tools/meta_tags/tags/title.rb +48 -0
- data/lib/para/seo_tools/meta_tags/tags/url.rb +13 -0
- data/lib/para/seo_tools/meta_tags/tags.rb +29 -0
- data/lib/para/seo_tools/meta_tags/vendors/base.rb +27 -0
- data/lib/para/seo_tools/meta_tags/vendors/open_graph.rb +23 -0
- data/lib/para/seo_tools/meta_tags/vendors/twitter.rb +23 -0
- data/lib/para/seo_tools/meta_tags/vendors.rb +17 -0
- data/lib/para/seo_tools/meta_tags.rb +12 -0
- data/lib/para/seo_tools/routes.rb +15 -0
- data/lib/para/seo_tools/sitemap.rb +13 -0
- data/lib/para/seo_tools/skeleton/page.rb +71 -0
- data/lib/para/seo_tools/skeleton/site.rb +18 -0
- data/lib/para/seo_tools/skeleton/worker.rb +11 -0
- data/lib/para/seo_tools/skeleton.rb +79 -0
- data/lib/para/seo_tools/version.rb +5 -0
- data/lib/para/seo_tools/view_helpers.rb +9 -0
- data/lib/para/seo_tools.rb +43 -0
- data/lib/tasks/migration.rake +20 -0
- data/lib/tasks/sitemap.rake +17 -0
- data/lib/tasks/skeleton.rake +9 -0
- data/para-seo_tools.gemspec +27 -0
- data/test/fixtures/seo_tools/pages.yml +11 -0
- data/test/models/seo_tools/page_test.rb +7 -0
- metadata +181 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
module MetaTags
|
4
|
+
class Renderer
|
5
|
+
LINE_SEPARATOR = "\n"
|
6
|
+
|
7
|
+
attr_reader :template, :vendors
|
8
|
+
|
9
|
+
delegate :content_tag, :tag, to: :template
|
10
|
+
|
11
|
+
def initialize(template, vendors: nil)
|
12
|
+
@template = template
|
13
|
+
@vendors = vendors
|
14
|
+
end
|
15
|
+
|
16
|
+
def render
|
17
|
+
[
|
18
|
+
charset_tag,
|
19
|
+
title_tag,
|
20
|
+
description_tag,
|
21
|
+
keywords_tag,
|
22
|
+
canonical_tag,
|
23
|
+
vendor_tags
|
24
|
+
].compact.join(LINE_SEPARATOR).html_safe
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def title_tag
|
30
|
+
if (title = meta_tags.title).present?
|
31
|
+
content_tag(:title, title)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def charset_tag
|
36
|
+
tag(:meta, charset: meta_tags.charset)
|
37
|
+
end
|
38
|
+
|
39
|
+
def description_tag
|
40
|
+
if (description = meta_tags.description).present?
|
41
|
+
tag(:meta, name: 'description', content: description)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def keywords_tag
|
46
|
+
if (keywords = meta_tags.keywords).present?
|
47
|
+
tag(:meta, name: 'keywords', content: keywords)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def canonical_tag
|
52
|
+
if (canonical = meta_tags.canonical).present?
|
53
|
+
tag(:link, rel: 'canonical', href: canonical)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def vendor_tags
|
58
|
+
return unless vendors
|
59
|
+
|
60
|
+
vendor_tags = vendors.each_with_object([]) do |vendor_name, tags|
|
61
|
+
vendor_class = MetaTags::Vendors.for(vendor_name)
|
62
|
+
vendor = vendor_class.new(template)
|
63
|
+
|
64
|
+
vendor.tags.each do |tag_name|
|
65
|
+
if (value = meta_tags.send(tag_name)).present?
|
66
|
+
tags << vendor.render(tag_name, value)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
vendor_tags.join(LINE_SEPARATOR)
|
72
|
+
end
|
73
|
+
|
74
|
+
def meta_tags
|
75
|
+
@meta_tags ||= template.controller.meta_tags_store
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
module MetaTags
|
4
|
+
class Store
|
5
|
+
attr_reader :controller, :defaults
|
6
|
+
|
7
|
+
TAGS = :title, :description, :image, :url, :site_name, :keywords, :type,
|
8
|
+
:site, :card, :charset, :canonical
|
9
|
+
|
10
|
+
attr_writer *TAGS
|
11
|
+
|
12
|
+
def initialize(controller)
|
13
|
+
@controller = controller
|
14
|
+
|
15
|
+
@defaults = if Para::SeoTools.defaults
|
16
|
+
controller.instance_exec(&Para::SeoTools.defaults)
|
17
|
+
else
|
18
|
+
{}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def charset
|
23
|
+
@charset ||= 'utf-8'
|
24
|
+
end
|
25
|
+
|
26
|
+
TAGS.each do |tag_name|
|
27
|
+
define_method(tag_name) do
|
28
|
+
fetch_value_for(tag_name)
|
29
|
+
end unless method_defined?(tag_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def fetch_value_for(tag_name)
|
35
|
+
ivar_name = :"@#{ tag_name }"
|
36
|
+
|
37
|
+
if (value = instance_variable_get(ivar_name)).present?
|
38
|
+
value
|
39
|
+
else
|
40
|
+
value = if (processor = MetaTags::Tags[tag_name])
|
41
|
+
processor.new(controller).value
|
42
|
+
end
|
43
|
+
|
44
|
+
value = defaults[tag_name] unless value.present?
|
45
|
+
|
46
|
+
instance_variable_set(ivar_name, value) if value
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
module MetaTags
|
4
|
+
module Tags
|
5
|
+
class Base
|
6
|
+
attr_accessor :controller
|
7
|
+
|
8
|
+
def initialize(controller)
|
9
|
+
@controller = controller
|
10
|
+
end
|
11
|
+
|
12
|
+
# Overriding this method in subclasses allow to process the meta tag
|
13
|
+
# value before it is rendered, e.g. stripping HTML tags
|
14
|
+
#
|
15
|
+
def self.process(value)
|
16
|
+
value
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def member_action?
|
22
|
+
controller.params.key?(:id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def action_i18n(label)
|
26
|
+
I18n.t(action_key(label), default: '').presence
|
27
|
+
end
|
28
|
+
|
29
|
+
def action_key(label)
|
30
|
+
key = %w(meta_tags controller)
|
31
|
+
key << controller.params[:controller]
|
32
|
+
key << controller.params[:action]
|
33
|
+
key << label
|
34
|
+
key.join(".")
|
35
|
+
end
|
36
|
+
|
37
|
+
# TODO : Define if we'll still support instance meta tags and refactor
|
38
|
+
# accordingly
|
39
|
+
def instance
|
40
|
+
end
|
41
|
+
|
42
|
+
def model_name
|
43
|
+
@model_name ||= controller.class.name.gsub(/Controller$/, '').singularize.underscore
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
module MetaTags
|
4
|
+
module Tags
|
5
|
+
class Description < Base
|
6
|
+
def value
|
7
|
+
self.class.process(
|
8
|
+
meta_taggable_description || instance_description || action_name
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.process(value)
|
13
|
+
Processor.new.process(value)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def meta_taggable_description
|
19
|
+
instance && instance.meta_tagged? &&
|
20
|
+
instance.meta_tags_list.meta_description.presence
|
21
|
+
end
|
22
|
+
|
23
|
+
def instance_description
|
24
|
+
if instance
|
25
|
+
Para::SeoTools.description_methods.each do |method|
|
26
|
+
if instance.respond_to?(method)
|
27
|
+
if (description = instance.send(method).presence)
|
28
|
+
return description
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
return nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def action_name
|
38
|
+
if (action_name = action_i18n(:description))
|
39
|
+
return action_name
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Processor
|
44
|
+
include ActionView::Helpers::TextHelper
|
45
|
+
|
46
|
+
def process(value)
|
47
|
+
truncate(sanitize(value, tags: []), length: 250)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
module MetaTags
|
4
|
+
module Tags
|
5
|
+
class Image < Base
|
6
|
+
def value
|
7
|
+
self.class.process(instance_image)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.process(value)
|
11
|
+
url = url_from(value)
|
12
|
+
|
13
|
+
if url.start_with?('http://')
|
14
|
+
url
|
15
|
+
else
|
16
|
+
request = RequestStore.store[:'para.seo_tools.request']
|
17
|
+
URI.join(request.url, url).to_s
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def instance_image
|
24
|
+
if member_action?
|
25
|
+
Para::SeoTools.image_methods.each do |method|
|
26
|
+
if instance.respond_to?(method) && instance.send(method)
|
27
|
+
return instance.send(method).url
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.url_from(value)
|
36
|
+
if Paperclip::Attachment === value
|
37
|
+
value.url(:thumb)
|
38
|
+
else
|
39
|
+
value.to_s
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
module MetaTags
|
4
|
+
module Tags
|
5
|
+
class Keywords < Base
|
6
|
+
def value
|
7
|
+
meta_taggable_keywords
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def meta_taggable_keywords
|
13
|
+
instance && instance.meta_tagged? &&
|
14
|
+
instance.meta_tags_list.meta_keywords.presence
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
module MetaTags
|
4
|
+
module Tags
|
5
|
+
class Title < Base
|
6
|
+
def value
|
7
|
+
meta_taggable_title || instance_title || action_name || model_name_translation
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def meta_taggable_title
|
13
|
+
instance && instance.meta_tagged? &&
|
14
|
+
instance.meta_tags_list.meta_title.presence
|
15
|
+
end
|
16
|
+
|
17
|
+
def instance_title
|
18
|
+
if instance
|
19
|
+
Para::SeoTools.title_methods.each do |method|
|
20
|
+
if instance.respond_to?(method) && (title = instance.send(method)).presence
|
21
|
+
return title
|
22
|
+
end
|
23
|
+
end
|
24
|
+
return nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def action_name
|
29
|
+
if (action_name = action_i18n(:title))
|
30
|
+
return action_name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def model_name_translation
|
35
|
+
return unless model_name
|
36
|
+
model_key = "activerecord.models.#{ model_name }"
|
37
|
+
|
38
|
+
if (translation = I18n.t("#{ model_key }.other", default: "").presence)
|
39
|
+
translation
|
40
|
+
elsif (translation = I18n.t(model_key, default: "").presence)
|
41
|
+
translation.pluralize
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
module MetaTags
|
4
|
+
module Tags
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
autoload :Base
|
8
|
+
autoload :Title
|
9
|
+
autoload :Description
|
10
|
+
autoload :Keywords
|
11
|
+
autoload :Image
|
12
|
+
autoload :Url
|
13
|
+
|
14
|
+
def self.[](tag_name)
|
15
|
+
processors[tag_name]
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def self.processors
|
21
|
+
@processors ||= [:title, :description, :keywords, :image, :url].reduce({}) do |hash, tag|
|
22
|
+
hash[tag] = MetaTags::Tags.const_get(tag.to_s.camelize)
|
23
|
+
hash
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
module MetaTags
|
4
|
+
module Vendors
|
5
|
+
class Base
|
6
|
+
include ActionView::Helpers::TagHelper
|
7
|
+
|
8
|
+
attr_reader :template
|
9
|
+
|
10
|
+
def initialize(template)
|
11
|
+
@template = template
|
12
|
+
end
|
13
|
+
|
14
|
+
def render(key, value)
|
15
|
+
tag(:meta, key_name => attribute_name_for(key), content: value)
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def attribute_name_for(key)
|
21
|
+
[namespace, key].join(':')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
module MetaTags
|
4
|
+
module Vendors
|
5
|
+
class OpenGraph < Para::SeoTools::MetaTags::Vendors::Base
|
6
|
+
def tags
|
7
|
+
[:title, :description, :image, :type, :url, :site_name]
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def key_name
|
13
|
+
'property'
|
14
|
+
end
|
15
|
+
|
16
|
+
def namespace
|
17
|
+
'og'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
module MetaTags
|
4
|
+
module Vendors
|
5
|
+
class Twitter < Para::SeoTools::MetaTags::Vendors::Base
|
6
|
+
def tags
|
7
|
+
[:title, :description, :image, :url, :site, :card]
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def key_name
|
13
|
+
'name'
|
14
|
+
end
|
15
|
+
|
16
|
+
def namespace
|
17
|
+
'twitter'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
module MetaTags
|
4
|
+
module Vendors
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
autoload :Base
|
8
|
+
autoload :OpenGraph
|
9
|
+
autoload :Twitter
|
10
|
+
|
11
|
+
def self.for(vendor_name)
|
12
|
+
const_get(vendor_name.to_s.camelize)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
class Sitemap
|
4
|
+
def self.build
|
5
|
+
::Sitemap::Generator.instance.load :host => Para::SeoTools.host do
|
6
|
+
Para::SeoTools::Skeleton.site.pages.each do |page|
|
7
|
+
literal page.path, page.sitemap_options
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
module Skeleton
|
4
|
+
class Page
|
5
|
+
include Rails.application.routes.url_helpers
|
6
|
+
|
7
|
+
attr_reader :name, :resource, :options, :locale
|
8
|
+
|
9
|
+
def initialize(name, path: nil, resource: nil, **options)
|
10
|
+
@name = name
|
11
|
+
@path = path
|
12
|
+
@resource = resource
|
13
|
+
@options = options
|
14
|
+
|
15
|
+
# Fetch locale on page build to allow calling the `page` skeleton
|
16
|
+
# method inside a `I18n.with_locale` block
|
17
|
+
#
|
18
|
+
@locale = options.fetch(:locale, I18n.locale)
|
19
|
+
end
|
20
|
+
|
21
|
+
def identifier
|
22
|
+
@identifier ||= [name, resource.try(:id)].compact.join(':')
|
23
|
+
end
|
24
|
+
|
25
|
+
def display_name
|
26
|
+
@display_name ||= [name, resource.try(:id)].compact.join(' #').humanize
|
27
|
+
end
|
28
|
+
|
29
|
+
def path
|
30
|
+
@path ||= find_route
|
31
|
+
end
|
32
|
+
|
33
|
+
def model
|
34
|
+
@model ||= self.class.model_for(identifier).tap do |page|
|
35
|
+
# Override path (i.e.: slug changed)
|
36
|
+
page.path = path if path.to_s != page.path
|
37
|
+
page.locale = locale
|
38
|
+
# Do not override meta tags if already present
|
39
|
+
page.defaults = options[:defaults] || {}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def sitemap_options
|
44
|
+
[:priority, :change_frequency].each_with_object({}) do |param, hash|
|
45
|
+
hash[param] = options[param] if options.key?(param)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Find a route matching the name of the page, passing the resource
|
52
|
+
# for member routes to work out of the box
|
53
|
+
def find_route
|
54
|
+
if respond_to?(:"#{ name }_path")
|
55
|
+
send(:"#{ name }_path", resource)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.model_for(identifier)
|
60
|
+
models[identifier] ||= ::Para::SeoTools::Page.new(identifier: identifier)
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.models
|
64
|
+
@models ||= Para::SeoTools::Page.all.each_with_object({}) do |page, hash|
|
65
|
+
hash[page.identifier] = page
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Para
|
2
|
+
module SeoTools
|
3
|
+
module Skeleton
|
4
|
+
class Site
|
5
|
+
include Rails.application.routes.url_helpers
|
6
|
+
|
7
|
+
def page(name, options = {} , &block)
|
8
|
+
page = Skeleton::Page.new(name, options)
|
9
|
+
pages << page
|
10
|
+
end
|
11
|
+
|
12
|
+
def pages
|
13
|
+
@pages ||= []
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|