maglevcms 1.2.2 → 1.4.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.
- checksums.yaml +4 -4
- data/app/components/maglev/content/builder.rb +3 -1
- data/app/components/maglev/content/checkbox.rb +7 -1
- data/app/components/maglev/content/void.rb +11 -0
- data/app/controllers/concerns/maglev/fetchers_concern.rb +2 -1
- data/app/controllers/concerns/maglev/rendering_concern.rb +0 -2
- data/app/controllers/maglev/editor_controller.rb +4 -0
- data/app/frontend/admin/controllers/screenshot_controller.js +1 -1
- data/app/frontend/editor/components/dynamic-form/dynamic-input.vue +30 -10
- data/app/frontend/editor/components/dynamic-form/index.vue +2 -0
- data/app/frontend/editor/components/kit/color-input/core-input.vue +1 -1
- data/app/frontend/editor/components/kit/color-input.vue +1 -1
- data/app/frontend/editor/components/kit/divider.vue +22 -0
- data/app/frontend/editor/components/kit/hint.vue +12 -0
- data/app/frontend/editor/components/kit/index.js +4 -0
- data/app/frontend/editor/components/page/form/seo.vue +1 -1
- data/app/frontend/editor/components/section-block-pane/setting-list.vue +4 -0
- data/app/frontend/editor/components/section-highlighter/top-left-actions.vue +7 -1
- data/app/frontend/editor/components/section-list/list-item.vue +6 -1
- data/app/frontend/editor/components/section-pane/index.vue +2 -1
- data/app/frontend/editor/components/section-pane/setting-list.vue +4 -0
- data/app/frontend/editor/components/style-pane/index.vue +4 -0
- data/app/frontend/editor/locales/editor.fr.json +8 -2
- data/app/frontend/editor/locales/editor.pt-BR.json +257 -0
- data/app/frontend/editor/locales/index.js +3 -0
- data/app/frontend/editor/mixins/global.js +16 -0
- data/app/frontend/editor/router/index.js +1 -2
- data/app/frontend/editor/views/content-pane.vue +2 -2
- data/app/models/concerns/maglev/sections_concern.rb +17 -13
- data/app/models/concerns/maglev/translatable.rb +4 -0
- data/app/models/maglev/page.rb +6 -0
- data/app/models/maglev/section/setting.rb +22 -25
- data/app/models/maglev/setting_types/base.rb +10 -0
- data/app/models/maglev/setting_types/checkbox.rb +9 -0
- data/app/models/maglev/setting_types/collection_item.rb +13 -0
- data/app/models/maglev/setting_types/color.rb +6 -0
- data/app/models/maglev/setting_types/divider.rb +9 -0
- data/app/models/maglev/setting_types/hint.rb +9 -0
- data/app/models/maglev/setting_types/icon.rb +9 -0
- data/app/models/maglev/setting_types/image.rb +13 -0
- data/app/models/maglev/setting_types/link.rb +13 -0
- data/app/models/maglev/setting_types/select.rb +6 -0
- data/app/models/maglev/setting_types/text.rb +6 -0
- data/app/models/maglev/site/locales_concern.rb +12 -1
- data/app/models/maglev/site.rb +4 -0
- data/app/models/maglev/theme/style_setting.rb +1 -1
- data/app/models/maglev/theme.rb +35 -0
- data/app/services/maglev/add_site_locale.rb +64 -0
- data/app/services/maglev/app_container.rb +1 -0
- data/app/services/maglev/change_site_locales.rb +1 -1
- data/app/services/maglev/fetch_page.rb +7 -1
- data/app/services/maglev/fetch_section_screenshot_path.rb +1 -1
- data/app/services/maglev/get_page_fullpath.rb +5 -1
- data/app/services/maglev/persist_page.rb +15 -11
- data/lib/maglev/active_storage.rb +2 -2
- data/lib/maglev/engine.rb +6 -2
- data/lib/maglev/errors.rb +7 -0
- data/lib/maglev/preview_constraint.rb +15 -1
- data/lib/maglev/version.rb +1 -1
- metadata +23 -7
data/app/models/maglev/site.rb
CHANGED
|
@@ -10,7 +10,7 @@ class Maglev::Theme::StyleSetting
|
|
|
10
10
|
|
|
11
11
|
## validations ##
|
|
12
12
|
validates :id, :label, :type, :default, 'maglev/presence': true
|
|
13
|
-
validates :type, inclusion: { in: %w[text color select checkbox] }
|
|
13
|
+
validates :type, inclusion: { in: %w[text color select checkbox divider hint] }
|
|
14
14
|
|
|
15
15
|
## methods ##
|
|
16
16
|
|
data/app/models/maglev/theme.rb
CHANGED
|
@@ -12,5 +12,40 @@ module Maglev
|
|
|
12
12
|
validates :id, :name, presence: true
|
|
13
13
|
|
|
14
14
|
## methods ##
|
|
15
|
+
|
|
16
|
+
def find_setting!(section_id, block_id, setting_id)
|
|
17
|
+
setting = find_setting(section_id, block_id, setting_id)
|
|
18
|
+
raise Maglev::Errors::UnknownSetting.new(section_id, block_id, setting_id) if setting.nil?
|
|
19
|
+
|
|
20
|
+
setting
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def find_setting(section_id, block_id, setting_id)
|
|
24
|
+
key = [section_id, block_id, setting_id].compact.join('.')
|
|
25
|
+
section_setting_types[key]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def section_setting_types
|
|
31
|
+
@section_setting_types ||= build_section_setting_types
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build_section_setting_types
|
|
35
|
+
hash = {}
|
|
36
|
+
sections.each do |section|
|
|
37
|
+
build_section_setting_types_from_settings(hash, section.id, section.settings)
|
|
38
|
+
section.blocks.each do |block|
|
|
39
|
+
build_section_setting_types_from_settings(hash, "#{section.id}.#{block.type}", block.settings)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
hash
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build_section_setting_types_from_settings(hash, parent_key, settings)
|
|
46
|
+
settings.each do |setting|
|
|
47
|
+
hash["#{parent_key}.#{setting.id}"] = setting
|
|
48
|
+
end
|
|
49
|
+
end
|
|
15
50
|
end
|
|
16
51
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Maglev
|
|
4
|
+
# Add a locale (instace of Maglev::Site::Locale) to a site
|
|
5
|
+
class AddSiteLocale
|
|
6
|
+
include Injectable
|
|
7
|
+
|
|
8
|
+
argument :site
|
|
9
|
+
argument :locale
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
return if locale.blank? || !site.add_locale(locale)
|
|
13
|
+
|
|
14
|
+
ActiveRecord::Base.transaction do
|
|
15
|
+
unsafe_call
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
protected
|
|
20
|
+
|
|
21
|
+
def unsafe_call
|
|
22
|
+
# Set a default content for site_scoped sections
|
|
23
|
+
apply_to_site
|
|
24
|
+
|
|
25
|
+
site.save! # persist the new locale
|
|
26
|
+
|
|
27
|
+
# add a default content in the new locale to all the pages of the site
|
|
28
|
+
# based on the default locale. Also take care of the path.
|
|
29
|
+
apply_to_pages
|
|
30
|
+
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def apply_to_site
|
|
35
|
+
Maglev::I18n.with_locale(locale.prefix) do
|
|
36
|
+
site.translate_in(locale.prefix, site.default_locale_prefix)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def apply_to_pages
|
|
41
|
+
resources.find_each do |page|
|
|
42
|
+
# the path will be the same as in the default locale
|
|
43
|
+
Maglev::I18n.with_locale(locale.prefix) do
|
|
44
|
+
page.path = default_page_path(page)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# set a default content which will be the same as in the default locale
|
|
48
|
+
page.translate_in(locale.prefix, site.default_locale_prefix)
|
|
49
|
+
|
|
50
|
+
page.save!
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def default_page_path(page)
|
|
55
|
+
# we can't rely on page.default_path because the service can be called outside a HTTP request
|
|
56
|
+
# and so we don't know for sure if Maglev::I18n.default_locale has been correctly set.
|
|
57
|
+
page.paths.find_by(locale: site.default_locale.prefix)&.value
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def resources
|
|
61
|
+
::Maglev::Page
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -31,6 +31,7 @@ module Maglev
|
|
|
31
31
|
dependency :persist_section_screenshot, class: Maglev::PersistSectionScreenshot,
|
|
32
32
|
depends_on: %i[fetch_theme fetch_section_screenshot_path]
|
|
33
33
|
|
|
34
|
+
dependency :add_site_locale, class: Maglev::AddSiteLocale
|
|
34
35
|
dependency :change_site_locales, class: Maglev::ChangeSiteLocales
|
|
35
36
|
|
|
36
37
|
dependency :fetch_page, class: Maglev::FetchPage, depends_on: :fetch_site
|
|
@@ -10,6 +10,7 @@ module Maglev
|
|
|
10
10
|
argument :locale
|
|
11
11
|
argument :default_locale
|
|
12
12
|
argument :fallback_to_default_locale, default: false
|
|
13
|
+
argument :only_visible, default: false
|
|
13
14
|
|
|
14
15
|
def call
|
|
15
16
|
page = fetch_page(path, locale)
|
|
@@ -20,7 +21,12 @@ module Maglev
|
|
|
20
21
|
protected
|
|
21
22
|
|
|
22
23
|
def fetch_page(path, locale)
|
|
23
|
-
|
|
24
|
+
page = pages.by_path(path || 'index', locale).first
|
|
25
|
+
!only_visible || (page&.visible? && only_visible) ? page : nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def pages
|
|
29
|
+
Maglev::Page
|
|
24
30
|
end
|
|
25
31
|
end
|
|
26
32
|
end
|
|
@@ -11,7 +11,7 @@ module Maglev
|
|
|
11
11
|
|
|
12
12
|
def call
|
|
13
13
|
path = "#{fetch_sections_path.call(theme: theme)}/#{section.category}/#{section.id}.jpg"
|
|
14
|
-
absolute ?
|
|
14
|
+
absolute ? Rails.root.join("public/#{path}").to_s : "/#{path}"
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
end
|
|
@@ -44,7 +44,7 @@ module Maglev
|
|
|
44
44
|
|
|
45
45
|
def build_fullpath(base_url, path)
|
|
46
46
|
fullpath = [base_url]
|
|
47
|
-
fullpath.push(locale)
|
|
47
|
+
fullpath.push(locale) if prefix_by_default_locale?
|
|
48
48
|
fullpath.push(path) unless path == 'index' # for SEO purpose)
|
|
49
49
|
fullpath.push(nil) if fullpath == [nil] # avoid "" as the fullpath
|
|
50
50
|
fullpath.join('/')
|
|
@@ -54,6 +54,10 @@ module Maglev
|
|
|
54
54
|
@site ||= fetch_site.call
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
def prefix_by_default_locale?
|
|
58
|
+
!same_as_default_locale?
|
|
59
|
+
end
|
|
60
|
+
|
|
57
61
|
def same_as_default_locale?
|
|
58
62
|
locale.to_s == default_locale.to_s
|
|
59
63
|
end
|
|
@@ -29,9 +29,9 @@ module Maglev
|
|
|
29
29
|
# the sections_translations attribute is put by the SetupPages service
|
|
30
30
|
# when we generate a brand new site
|
|
31
31
|
if page_attributes.key?(:sections_translations)
|
|
32
|
-
page.prepare_sections_translations
|
|
32
|
+
page.prepare_sections_translations(theme)
|
|
33
33
|
else
|
|
34
|
-
page.prepare_sections
|
|
34
|
+
page.prepare_sections(theme)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
page.save!
|
|
@@ -40,15 +40,7 @@ module Maglev
|
|
|
40
40
|
def persist_site!
|
|
41
41
|
return unless can_persist_site?
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
# when we generate a brand new site
|
|
45
|
-
if site_attributes.key?(:sections_translations)
|
|
46
|
-
site.attributes = site_attributes
|
|
47
|
-
site.prepare_sections_translations
|
|
48
|
-
else
|
|
49
|
-
site.attributes = site_attributes_with_consistent_sections
|
|
50
|
-
site.prepare_sections
|
|
51
|
-
end
|
|
43
|
+
assign_sections_to_site
|
|
52
44
|
|
|
53
45
|
site.save!
|
|
54
46
|
end
|
|
@@ -89,5 +81,17 @@ module Maglev
|
|
|
89
81
|
site_attributes[:sections_translations].present?
|
|
90
82
|
)
|
|
91
83
|
end
|
|
84
|
+
|
|
85
|
+
def assign_sections_to_site
|
|
86
|
+
# the sections_translations attribute is put by the SetupPages service
|
|
87
|
+
# when we generate a brand new site
|
|
88
|
+
if site_attributes.key?(:sections_translations)
|
|
89
|
+
site.attributes = site_attributes
|
|
90
|
+
site.prepare_sections_translations(theme)
|
|
91
|
+
else
|
|
92
|
+
site.attributes = site_attributes_with_consistent_sections
|
|
93
|
+
site.prepare_sections(theme)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
92
96
|
end
|
|
93
97
|
end
|
data/lib/maglev/engine.rb
CHANGED
|
@@ -54,7 +54,9 @@ module Maglev
|
|
|
54
54
|
urls: ["/#{vite_ruby.config.public_output_dir}"],
|
|
55
55
|
root: root.join(vite_ruby.config.public_dir),
|
|
56
56
|
header_rules: [
|
|
57
|
-
|
|
57
|
+
# rubocop:disable Style/StringHashKeys
|
|
58
|
+
[:all, { 'Access-Control-Allow-Origin' => '*' }]
|
|
59
|
+
# rubocop:enable Style/StringHashKeys
|
|
58
60
|
]
|
|
59
61
|
else
|
|
60
62
|
# mostly when running the application in production behind NGINX or APACHE
|
|
@@ -63,7 +65,9 @@ module Maglev
|
|
|
63
65
|
urls: ["/#{vite_ruby.config.public_output_dir}"],
|
|
64
66
|
root: root.join(vite_ruby.config.public_dir),
|
|
65
67
|
header_rules: [
|
|
66
|
-
|
|
68
|
+
# rubocop:disable Style/StringHashKeys
|
|
69
|
+
[:all, { 'Access-Control-Allow-Origin' => '*' }]
|
|
70
|
+
# rubocop:enable Style/StringHashKeys
|
|
67
71
|
]
|
|
68
72
|
end
|
|
69
73
|
end
|
data/lib/maglev/errors.rb
CHANGED
|
@@ -4,5 +4,12 @@ module Maglev
|
|
|
4
4
|
module Errors
|
|
5
5
|
class NotAuthorized < StandardError; end
|
|
6
6
|
class UnknownSection < StandardError; end
|
|
7
|
+
|
|
8
|
+
class UnknownSetting < StandardError
|
|
9
|
+
def initialize(section_id, block_id, setting_id)
|
|
10
|
+
key = [section_id, block_id].compact.join('.')
|
|
11
|
+
super("[#{key}] The #{setting_id} setting is undeclared OR its type is unknown.")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
7
14
|
end
|
|
8
15
|
end
|
|
@@ -4,6 +4,8 @@ require 'uri'
|
|
|
4
4
|
|
|
5
5
|
module Maglev
|
|
6
6
|
class PreviewConstraint
|
|
7
|
+
CRAWLER_USER_AGENTS = /Googlebot|Twitterbot|facebookexternalhit/o.freeze
|
|
8
|
+
|
|
7
9
|
attr_reader :preview_host
|
|
8
10
|
|
|
9
11
|
def initialize(preview_host: nil)
|
|
@@ -11,7 +13,7 @@ module Maglev
|
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def matches?(request)
|
|
14
|
-
|
|
16
|
+
(accepted_format?(request) || crawler?(request)) && match_host?(request)
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
protected
|
|
@@ -21,5 +23,17 @@ module Maglev
|
|
|
21
23
|
|
|
22
24
|
URI.parse(Maglev.config.preview_host).host # make sure we get only the host here
|
|
23
25
|
end
|
|
26
|
+
|
|
27
|
+
def accepted_format?(request)
|
|
28
|
+
%i[html xml].include?(request.format.symbol)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def crawler?(request)
|
|
32
|
+
request.format.symbol.nil? && CRAWLER_USER_AGENTS.match?(request.user_agent)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def match_host?(request)
|
|
36
|
+
!preview_host || preview_host == request.host
|
|
37
|
+
end
|
|
24
38
|
end
|
|
25
39
|
end
|
data/lib/maglev/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: maglevcms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Didier Lafforgue
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-
|
|
11
|
+
date: 2023-11-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jbuilder
|
|
@@ -56,9 +56,9 @@ dependencies:
|
|
|
56
56
|
name: rails
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
|
-
- - "
|
|
59
|
+
- - "<"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '7.
|
|
61
|
+
version: '7.2'
|
|
62
62
|
- - ">="
|
|
63
63
|
- !ruby/object:Gem::Version
|
|
64
64
|
version: '6'
|
|
@@ -66,9 +66,9 @@ dependencies:
|
|
|
66
66
|
prerelease: false
|
|
67
67
|
version_requirements: !ruby/object:Gem::Requirement
|
|
68
68
|
requirements:
|
|
69
|
-
- - "
|
|
69
|
+
- - "<"
|
|
70
70
|
- !ruby/object:Gem::Version
|
|
71
|
-
version: '7.
|
|
71
|
+
version: '7.2'
|
|
72
72
|
- - ">="
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
74
|
version: '6'
|
|
@@ -133,6 +133,7 @@ files:
|
|
|
133
133
|
- app/components/maglev/content/link.rb
|
|
134
134
|
- app/components/maglev/content/select.rb
|
|
135
135
|
- app/components/maglev/content/text.rb
|
|
136
|
+
- app/components/maglev/content/void.rb
|
|
136
137
|
- app/components/maglev/page_component.rb
|
|
137
138
|
- app/components/maglev/section_component.rb
|
|
138
139
|
- app/components/maglev/tag_helper.rb
|
|
@@ -551,7 +552,9 @@ files:
|
|
|
551
552
|
- app/frontend/editor/components/kit/color-input/preset-button.vue
|
|
552
553
|
- app/frontend/editor/components/kit/color-input/preset-dropdown.vue
|
|
553
554
|
- app/frontend/editor/components/kit/confirmation-button.vue
|
|
555
|
+
- app/frontend/editor/components/kit/divider.vue
|
|
554
556
|
- app/frontend/editor/components/kit/dropdown.vue
|
|
557
|
+
- app/frontend/editor/components/kit/hint.vue
|
|
555
558
|
- app/frontend/editor/components/kit/icon-input.vue
|
|
556
559
|
- app/frontend/editor/components/kit/icon.vue
|
|
557
560
|
- app/frontend/editor/components/kit/image-input.vue
|
|
@@ -625,6 +628,7 @@ files:
|
|
|
625
628
|
- app/frontend/editor/locales/editor.en.json
|
|
626
629
|
- app/frontend/editor/locales/editor.es.json
|
|
627
630
|
- app/frontend/editor/locales/editor.fr.json
|
|
631
|
+
- app/frontend/editor/locales/editor.pt-BR.json
|
|
628
632
|
- app/frontend/editor/locales/index.js
|
|
629
633
|
- app/frontend/editor/main.js
|
|
630
634
|
- app/frontend/editor/misc/__tests__/utils.spec.js
|
|
@@ -719,6 +723,17 @@ files:
|
|
|
719
723
|
- app/models/maglev/section/block.rb
|
|
720
724
|
- app/models/maglev/section/content_concern.rb
|
|
721
725
|
- app/models/maglev/section/setting.rb
|
|
726
|
+
- app/models/maglev/setting_types/base.rb
|
|
727
|
+
- app/models/maglev/setting_types/checkbox.rb
|
|
728
|
+
- app/models/maglev/setting_types/collection_item.rb
|
|
729
|
+
- app/models/maglev/setting_types/color.rb
|
|
730
|
+
- app/models/maglev/setting_types/divider.rb
|
|
731
|
+
- app/models/maglev/setting_types/hint.rb
|
|
732
|
+
- app/models/maglev/setting_types/icon.rb
|
|
733
|
+
- app/models/maglev/setting_types/image.rb
|
|
734
|
+
- app/models/maglev/setting_types/link.rb
|
|
735
|
+
- app/models/maglev/setting_types/select.rb
|
|
736
|
+
- app/models/maglev/setting_types/text.rb
|
|
722
737
|
- app/models/maglev/site.rb
|
|
723
738
|
- app/models/maglev/site/locale.rb
|
|
724
739
|
- app/models/maglev/site/locales_concern.rb
|
|
@@ -730,6 +745,7 @@ files:
|
|
|
730
745
|
- app/services/concerns/maglev/get_page_sections/transform_collection_item_concern.rb
|
|
731
746
|
- app/services/concerns/maglev/get_page_sections/transform_link_concern.rb
|
|
732
747
|
- app/services/concerns/maglev/get_page_sections/transform_text_concern.rb
|
|
748
|
+
- app/services/maglev/add_site_locale.rb
|
|
733
749
|
- app/services/maglev/app_container.rb
|
|
734
750
|
- app/services/maglev/change_site_locales.rb
|
|
735
751
|
- app/services/maglev/clone_page.rb
|
|
@@ -845,7 +861,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
845
861
|
- !ruby/object:Gem::Version
|
|
846
862
|
version: '0'
|
|
847
863
|
requirements: []
|
|
848
|
-
rubygems_version: 3.
|
|
864
|
+
rubygems_version: 3.3.26
|
|
849
865
|
signing_key:
|
|
850
866
|
specification_version: 4
|
|
851
867
|
summary: Page builder Ruby on Rails
|