not_pressed-core 0.1.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 +7 -0
- data/LICENSE.txt +41 -0
- data/README.md +285 -0
- data/app/assets/javascripts/not_pressed/lightbox.js +110 -0
- data/app/assets/stylesheets/not_pressed/admin.css +1161 -0
- data/app/assets/stylesheets/not_pressed/content.css +193 -0
- data/app/assets/stylesheets/not_pressed/gallery.css +117 -0
- data/app/assets/stylesheets/not_pressed/themes/starter.css +118 -0
- data/app/controllers/not_pressed/admin/base_controller.rb +21 -0
- data/app/controllers/not_pressed/admin/categories_controller.rb +53 -0
- data/app/controllers/not_pressed/admin/content_blocks_controller.rb +73 -0
- data/app/controllers/not_pressed/admin/dashboard_controller.rb +19 -0
- data/app/controllers/not_pressed/admin/forms_controller.rb +86 -0
- data/app/controllers/not_pressed/admin/media_attachments_controller.rb +94 -0
- data/app/controllers/not_pressed/admin/pages_controller.rb +122 -0
- data/app/controllers/not_pressed/admin/plugins_controller.rb +121 -0
- data/app/controllers/not_pressed/admin/settings_controller.rb +19 -0
- data/app/controllers/not_pressed/admin/tags_controller.rb +37 -0
- data/app/controllers/not_pressed/admin/themes_controller.rb +104 -0
- data/app/controllers/not_pressed/application_controller.rb +6 -0
- data/app/controllers/not_pressed/blog_controller.rb +83 -0
- data/app/controllers/not_pressed/form_submissions_controller.rb +70 -0
- data/app/controllers/not_pressed/pages_controller.rb +36 -0
- data/app/controllers/not_pressed/robots_controller.rb +34 -0
- data/app/controllers/not_pressed/sitemaps_controller.rb +12 -0
- data/app/helpers/not_pressed/admin_helper.rb +41 -0
- data/app/helpers/not_pressed/application_helper.rb +6 -0
- data/app/helpers/not_pressed/code_injection_helper.rb +29 -0
- data/app/helpers/not_pressed/content_helper.rb +13 -0
- data/app/helpers/not_pressed/form_helper.rb +80 -0
- data/app/helpers/not_pressed/media_helper.rb +28 -0
- data/app/helpers/not_pressed/seo_helper.rb +69 -0
- data/app/helpers/not_pressed/theme_helper.rb +42 -0
- data/app/mailers/not_pressed/application_mailer.rb +10 -0
- data/app/mailers/not_pressed/form_mailer.rb +15 -0
- data/app/models/concerns/not_pressed/sluggable.rb +43 -0
- data/app/models/not_pressed/category.rb +16 -0
- data/app/models/not_pressed/content_block.rb +46 -0
- data/app/models/not_pressed/form.rb +55 -0
- data/app/models/not_pressed/form_field.rb +23 -0
- data/app/models/not_pressed/form_submission.rb +19 -0
- data/app/models/not_pressed/media_attachment.rb +68 -0
- data/app/models/not_pressed/page.rb +182 -0
- data/app/models/not_pressed/page_version.rb +15 -0
- data/app/models/not_pressed/setting.rb +20 -0
- data/app/models/not_pressed/tag.rb +15 -0
- data/app/models/not_pressed/tagging.rb +12 -0
- data/app/plugins/not_pressed/analytics_plugin.rb +106 -0
- data/app/plugins/not_pressed/callout_block_plugin.rb +43 -0
- data/app/themes/not_pressed/starter_theme.rb +26 -0
- data/app/views/layouts/not_pressed/admin.html.erb +745 -0
- data/app/views/layouts/not_pressed/application.html.erb +12 -0
- data/app/views/layouts/not_pressed/page.html.erb +22 -0
- data/app/views/not_pressed/admin/categories/index.html.erb +58 -0
- data/app/views/not_pressed/admin/content_blocks/_block.html.erb +18 -0
- data/app/views/not_pressed/admin/content_blocks/_block_picker.html.erb +32 -0
- data/app/views/not_pressed/admin/content_blocks/create.turbo_stream.erb +3 -0
- data/app/views/not_pressed/admin/content_blocks/destroy.turbo_stream.erb +1 -0
- data/app/views/not_pressed/admin/content_blocks/editors/_callout.html.erb +38 -0
- data/app/views/not_pressed/admin/content_blocks/editors/_code.html.erb +26 -0
- data/app/views/not_pressed/admin/content_blocks/editors/_divider.html.erb +4 -0
- data/app/views/not_pressed/admin/content_blocks/editors/_form.html.erb +16 -0
- data/app/views/not_pressed/admin/content_blocks/editors/_gallery.html.erb +75 -0
- data/app/views/not_pressed/admin/content_blocks/editors/_heading.html.erb +26 -0
- data/app/views/not_pressed/admin/content_blocks/editors/_html.html.erb +13 -0
- data/app/views/not_pressed/admin/content_blocks/editors/_image.html.erb +56 -0
- data/app/views/not_pressed/admin/content_blocks/editors/_quote.html.erb +24 -0
- data/app/views/not_pressed/admin/content_blocks/editors/_text.html.erb +28 -0
- data/app/views/not_pressed/admin/content_blocks/editors/_video.html.erb +25 -0
- data/app/views/not_pressed/admin/dashboard/index.html.erb +60 -0
- data/app/views/not_pressed/admin/forms/_field_row.html.erb +33 -0
- data/app/views/not_pressed/admin/forms/_form.html.erb +75 -0
- data/app/views/not_pressed/admin/forms/edit.html.erb +1 -0
- data/app/views/not_pressed/admin/forms/index.html.erb +32 -0
- data/app/views/not_pressed/admin/forms/new.html.erb +1 -0
- data/app/views/not_pressed/admin/forms/submissions.html.erb +34 -0
- data/app/views/not_pressed/admin/media_attachments/_media_card.html.erb +21 -0
- data/app/views/not_pressed/admin/media_attachments/_picker.html.erb +19 -0
- data/app/views/not_pressed/admin/media_attachments/edit.html.erb +57 -0
- data/app/views/not_pressed/admin/media_attachments/index.html.erb +48 -0
- data/app/views/not_pressed/admin/pages/_form.html.erb +177 -0
- data/app/views/not_pressed/admin/pages/_page_tree_node.html.erb +32 -0
- data/app/views/not_pressed/admin/pages/edit.html.erb +69 -0
- data/app/views/not_pressed/admin/pages/index.html.erb +21 -0
- data/app/views/not_pressed/admin/pages/new.html.erb +1 -0
- data/app/views/not_pressed/admin/pages/preview.html.erb +17 -0
- data/app/views/not_pressed/admin/plugins/_settings_form.html.erb +59 -0
- data/app/views/not_pressed/admin/plugins/index.html.erb +48 -0
- data/app/views/not_pressed/admin/plugins/show.html.erb +54 -0
- data/app/views/not_pressed/admin/settings/code_injection.html.erb +21 -0
- data/app/views/not_pressed/admin/shared/_breadcrumbs.html.erb +14 -0
- data/app/views/not_pressed/admin/shared/_flash.html.erb +7 -0
- data/app/views/not_pressed/admin/shared/_modal.html.erb +9 -0
- data/app/views/not_pressed/admin/shared/_sidebar.html.erb +18 -0
- data/app/views/not_pressed/admin/tags/index.html.erb +52 -0
- data/app/views/not_pressed/admin/themes/index.html.erb +60 -0
- data/app/views/not_pressed/admin/themes/show.html.erb +66 -0
- data/app/views/not_pressed/blog/_post_card.html.erb +24 -0
- data/app/views/not_pressed/blog/feed.rss.builder +22 -0
- data/app/views/not_pressed/blog/index.html.erb +56 -0
- data/app/views/not_pressed/blog/show.html.erb +41 -0
- data/app/views/not_pressed/form_mailer/submission_notification.text.erb +8 -0
- data/app/views/not_pressed/pages/show.html.erb +4 -0
- data/app/views/themes/starter/layouts/not_pressed/default.html.erb +36 -0
- data/app/views/themes/starter/layouts/not_pressed/full_width.html.erb +36 -0
- data/app/views/themes/starter/layouts/not_pressed/sidebar.html.erb +41 -0
- data/config/routes.rb +81 -0
- data/db/migrate/20260310000001_create_not_pressed_pages.rb +20 -0
- data/db/migrate/20260310000002_create_not_pressed_content_blocks.rb +17 -0
- data/db/migrate/20260310000003_create_not_pressed_media_attachments.rb +14 -0
- data/db/migrate/20260310000004_add_content_type_to_not_pressed_pages.rb +8 -0
- data/db/migrate/20260310000005_add_publishing_fields_to_not_pressed_pages.rb +8 -0
- data/db/migrate/20260310000006_create_not_pressed_page_versions.rb +16 -0
- data/db/migrate/20260310000007_add_settings_fields_to_not_pressed_pages.rb +11 -0
- data/db/migrate/20260311000001_create_not_pressed_forms.rb +42 -0
- data/db/migrate/20260311000002_add_seo_fields_to_not_pressed_pages.rb +10 -0
- data/db/migrate/20260311000003_add_code_injection_to_not_pressed_pages.rb +8 -0
- data/db/migrate/20260311000004_create_not_pressed_settings.rb +14 -0
- data/db/migrate/20260312000001_create_not_pressed_categories.rb +16 -0
- data/db/migrate/20260312000002_create_not_pressed_tags.rb +15 -0
- data/db/migrate/20260312000003_create_not_pressed_taggings.rb +14 -0
- data/db/migrate/20260312000004_add_category_id_to_not_pressed_pages.rb +7 -0
- data/lib/generators/not_pressed/install/install_generator.rb +52 -0
- data/lib/generators/not_pressed/install/templates/initializer.rb.tt +89 -0
- data/lib/generators/not_pressed/install/templates/seeds.rb.tt +131 -0
- data/lib/generators/not_pressed/plugin/plugin_generator.rb +37 -0
- data/lib/generators/not_pressed/plugin/templates/plugin.rb.tt +23 -0
- data/lib/not_pressed/admin/authentication.rb +48 -0
- data/lib/not_pressed/admin/menu_registry.rb +100 -0
- data/lib/not_pressed/configuration.rb +77 -0
- data/lib/not_pressed/content_type.rb +23 -0
- data/lib/not_pressed/content_type_builder.rb +51 -0
- data/lib/not_pressed/content_type_registry.rb +45 -0
- data/lib/not_pressed/engine.rb +132 -0
- data/lib/not_pressed/hooks.rb +166 -0
- data/lib/not_pressed/navigation/builder.rb +148 -0
- data/lib/not_pressed/navigation/menu.rb +54 -0
- data/lib/not_pressed/navigation/menu_item.rb +33 -0
- data/lib/not_pressed/navigation/node.rb +45 -0
- data/lib/not_pressed/navigation/partial_parser.rb +96 -0
- data/lib/not_pressed/navigation/route_inspector.rb +98 -0
- data/lib/not_pressed/navigation.rb +6 -0
- data/lib/not_pressed/plugin.rb +354 -0
- data/lib/not_pressed/plugin_importer.rb +133 -0
- data/lib/not_pressed/plugin_manager.rb +196 -0
- data/lib/not_pressed/plugin_packager.rb +129 -0
- data/lib/not_pressed/rendering/block_renderer.rb +222 -0
- data/lib/not_pressed/rendering/renderer_registry.rb +154 -0
- data/lib/not_pressed/rendering.rb +8 -0
- data/lib/not_pressed/seo/sitemap_builder.rb +61 -0
- data/lib/not_pressed/theme.rb +191 -0
- data/lib/not_pressed/theme_importer.rb +133 -0
- data/lib/not_pressed/theme_packager.rb +180 -0
- data/lib/not_pressed/theme_registry.rb +123 -0
- data/lib/not_pressed/version.rb +5 -0
- data/lib/not_pressed.rb +65 -0
- metadata +258 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class RobotsController < ApplicationController
|
|
5
|
+
def show
|
|
6
|
+
render plain: robots_content, content_type: "text/plain"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def robots_content
|
|
12
|
+
custom = NotPressed.configuration.robots_txt
|
|
13
|
+
|
|
14
|
+
case custom
|
|
15
|
+
when Proc
|
|
16
|
+
custom.call(request)
|
|
17
|
+
when String
|
|
18
|
+
custom
|
|
19
|
+
else
|
|
20
|
+
default_robots_content
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def default_robots_content
|
|
25
|
+
sitemap_url = not_pressed.sitemap_url(host: request.host, port: request.port, protocol: request.protocol)
|
|
26
|
+
|
|
27
|
+
<<~ROBOTS
|
|
28
|
+
User-agent: *
|
|
29
|
+
Allow: /
|
|
30
|
+
Sitemap: #{sitemap_url}
|
|
31
|
+
ROBOTS
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class SitemapsController < ApplicationController
|
|
5
|
+
def show
|
|
6
|
+
expires_in 1.hour, public: true
|
|
7
|
+
|
|
8
|
+
builder = NotPressed::Seo::SitemapBuilder.new(base_url: root_url)
|
|
9
|
+
render xml: builder.generate
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
module AdminHelper
|
|
5
|
+
def admin_breadcrumb(label, path = nil)
|
|
6
|
+
@breadcrumbs ||= []
|
|
7
|
+
@breadcrumbs << { label: label, path: path }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def status_badge(status)
|
|
11
|
+
content_tag(:span, status.to_s.humanize, class: "np-badge np-badge--#{status}")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def admin_nav_item(label, path, icon: nil)
|
|
15
|
+
css_class = current_page?(path) ? "np-nav-active" : nil
|
|
16
|
+
link_to label, path, class: css_class
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def time_ago_short(time)
|
|
20
|
+
return "" if time.nil?
|
|
21
|
+
|
|
22
|
+
seconds = (Time.current - time).to_i
|
|
23
|
+
minutes = seconds / 60
|
|
24
|
+
hours = minutes / 60
|
|
25
|
+
days = hours / 24
|
|
26
|
+
weeks = days / 7
|
|
27
|
+
|
|
28
|
+
if seconds < 60
|
|
29
|
+
"#{seconds}s ago"
|
|
30
|
+
elsif minutes < 60
|
|
31
|
+
"#{minutes}m ago"
|
|
32
|
+
elsif hours < 24
|
|
33
|
+
"#{hours}h ago"
|
|
34
|
+
elsif days < 7
|
|
35
|
+
"#{days}d ago"
|
|
36
|
+
else
|
|
37
|
+
"#{weeks}w ago"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
module CodeInjectionHelper
|
|
5
|
+
def head_injection(page = nil)
|
|
6
|
+
parts = []
|
|
7
|
+
|
|
8
|
+
global = NotPressed.configuration.global_head_code
|
|
9
|
+
global = NotPressed::Setting.get("global_head_code") if global.nil? && defined?(NotPressed::Setting)
|
|
10
|
+
parts << global if global.present?
|
|
11
|
+
|
|
12
|
+
parts << page.head_code if page&.head_code.present?
|
|
13
|
+
|
|
14
|
+
parts.join("\n").html_safe
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def body_injection(page = nil)
|
|
18
|
+
parts = []
|
|
19
|
+
|
|
20
|
+
global = NotPressed.configuration.global_body_code
|
|
21
|
+
global = NotPressed::Setting.get("global_body_code") if global.nil? && defined?(NotPressed::Setting)
|
|
22
|
+
parts << global if global.present?
|
|
23
|
+
|
|
24
|
+
parts << page.body_code if page&.body_code.present?
|
|
25
|
+
|
|
26
|
+
parts.join("\n").html_safe
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
module ContentHelper
|
|
5
|
+
def render_page_content(page)
|
|
6
|
+
NotPressed::Rendering::BlockRenderer.render_page(page)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def render_block(block)
|
|
10
|
+
NotPressed::Rendering::BlockRenderer.render(block)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
module FormHelper
|
|
5
|
+
def render_form(form)
|
|
6
|
+
return "" unless form
|
|
7
|
+
|
|
8
|
+
fields_html = form.form_fields.ordered.map { |field| render_form_field(field) }.join("\n")
|
|
9
|
+
action_url = not_pressed.form_submissions_path(form_slug: form.slug)
|
|
10
|
+
token = respond_to?(:form_authenticity_token) ? form_authenticity_token : ""
|
|
11
|
+
token_field = token.present? ? %(\n<input type="hidden" name="authenticity_token" value="#{np_escape(token)}">) : ""
|
|
12
|
+
|
|
13
|
+
%(<form action="#{action_url}" method="post" class="np-public-form">#{token_field}
|
|
14
|
+
#{fields_html}
|
|
15
|
+
<div class="np-form-submit"><input type="submit" value="Submit" class="np-form-submit-btn"></div>
|
|
16
|
+
</form>).html_safe
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def render_form_field(field)
|
|
22
|
+
input_html = case field.field_type.to_s
|
|
23
|
+
when "text", "email", "number", "phone", "url", "date"
|
|
24
|
+
input_type = field.field_type_phone? ? "tel" : field.field_type.to_s
|
|
25
|
+
%(<input type="#{input_type}" name="submission[#{form_field_param_name(field)}]" id="#{form_field_id(field)}" class="np-form-input" placeholder="#{np_escape(field.placeholder.to_s)}"#{required_attr(field)}>)
|
|
26
|
+
when "textarea"
|
|
27
|
+
%(<textarea name="submission[#{form_field_param_name(field)}]" id="#{form_field_id(field)}" class="np-form-input" placeholder="#{np_escape(field.placeholder.to_s)}" rows="4"#{required_attr(field)}></textarea>)
|
|
28
|
+
when "select"
|
|
29
|
+
options = parse_options(field.options)
|
|
30
|
+
opts_html = %(<option value="">Select...</option>) + options.map { |o| %(<option value="#{np_escape(o)}">#{np_escape(o)}</option>) }.join
|
|
31
|
+
%(<select name="submission[#{form_field_param_name(field)}]" id="#{form_field_id(field)}" class="np-form-input"#{required_attr(field)}>#{opts_html}</select>)
|
|
32
|
+
when "radio"
|
|
33
|
+
options = parse_options(field.options)
|
|
34
|
+
options.map { |o|
|
|
35
|
+
%(<label class="np-form-radio-label"><input type="radio" name="submission[#{form_field_param_name(field)}]" value="#{np_escape(o)}"#{required_attr(field)}> #{np_escape(o)}</label>)
|
|
36
|
+
}.join("\n")
|
|
37
|
+
when "checkbox"
|
|
38
|
+
options = parse_options(field.options)
|
|
39
|
+
if options.any?
|
|
40
|
+
options.map { |o|
|
|
41
|
+
%(<label class="np-form-checkbox-label"><input type="checkbox" name="submission[#{form_field_param_name(field)}][]" value="#{np_escape(o)}"> #{np_escape(o)}</label>)
|
|
42
|
+
}.join("\n")
|
|
43
|
+
else
|
|
44
|
+
%(<input type="checkbox" name="submission[#{form_field_param_name(field)}]" id="#{form_field_id(field)}" value="1"#{required_attr(field)}>)
|
|
45
|
+
end
|
|
46
|
+
when "file_upload"
|
|
47
|
+
%(<input type="file" name="submission[#{form_field_param_name(field)}]" id="#{form_field_id(field)}" class="np-form-input"#{required_attr(field)}>)
|
|
48
|
+
else
|
|
49
|
+
%(<input type="text" name="submission[#{form_field_param_name(field)}]" id="#{form_field_id(field)}" class="np-form-input"#{required_attr(field)}>)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
%(<div class="np-form-field">
|
|
53
|
+
<label class="np-form-label" for="#{form_field_id(field)}">#{np_escape(field.label)}#{" *" if field.required?}</label>
|
|
54
|
+
#{input_html}
|
|
55
|
+
</div>)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def form_field_param_name(field)
|
|
59
|
+
np_escape(field.label.parameterize(separator: "_"))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def form_field_id(field)
|
|
63
|
+
"field_#{field.id}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def required_attr(field)
|
|
67
|
+
field.required? ? ' required="required"' : ""
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parse_options(options_string)
|
|
71
|
+
return [] if options_string.blank?
|
|
72
|
+
|
|
73
|
+
options_string.split("\n").map(&:strip).reject(&:blank?)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def np_escape(text)
|
|
77
|
+
ERB::Util.html_escape(text)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
module MediaHelper
|
|
5
|
+
def media_url(media_attachment, variant: nil)
|
|
6
|
+
return nil if media_attachment.nil? || !media_attachment.file.attached?
|
|
7
|
+
|
|
8
|
+
if variant
|
|
9
|
+
variant_obj = media_attachment.send(variant)
|
|
10
|
+
return nil unless variant_obj
|
|
11
|
+
|
|
12
|
+
url_for(variant_obj)
|
|
13
|
+
else
|
|
14
|
+
url_for(media_attachment.file)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def media_image_tag(media_attachment, variant: nil, **options)
|
|
19
|
+
return nil if media_attachment.nil? || !media_attachment.image?
|
|
20
|
+
|
|
21
|
+
src = media_url(media_attachment, variant: variant)
|
|
22
|
+
return nil unless src
|
|
23
|
+
|
|
24
|
+
options[:alt] ||= media_attachment.alt_text
|
|
25
|
+
tag.img(src: src, **options)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
module SeoHelper
|
|
5
|
+
def seo_meta_tags(page, request = nil)
|
|
6
|
+
return "".html_safe if page.nil?
|
|
7
|
+
|
|
8
|
+
tags = []
|
|
9
|
+
separator = NotPressed.configuration.seo_title_separator
|
|
10
|
+
site_name = NotPressed.configuration.site_name
|
|
11
|
+
|
|
12
|
+
# Title
|
|
13
|
+
title_text = page.meta_title_or_title
|
|
14
|
+
full_title = site_name.present? ? "#{title_text}#{separator}#{site_name}" : title_text
|
|
15
|
+
tags << content_tag(:title, full_title)
|
|
16
|
+
|
|
17
|
+
# Meta description
|
|
18
|
+
if page.meta_description.present?
|
|
19
|
+
tags << tag(:meta, name: "description", content: page.meta_description)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Robots
|
|
23
|
+
robots = page.meta_robots_tag
|
|
24
|
+
if robots.present?
|
|
25
|
+
tags << tag(:meta, name: "robots", content: robots)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Canonical URL
|
|
29
|
+
if request
|
|
30
|
+
tags << tag(:link, rel: "canonical", href: page.effective_canonical_url(request))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Open Graph tags
|
|
34
|
+
tags << tag(:meta, property: "og:title", content: title_text)
|
|
35
|
+
|
|
36
|
+
if page.meta_description.present?
|
|
37
|
+
tags << tag(:meta, property: "og:description", content: page.meta_description)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if page.og_image_url.present?
|
|
41
|
+
tags << tag(:meta, property: "og:image", content: page.og_image_url)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if request
|
|
45
|
+
tags << tag(:meta, property: "og:url", content: page.effective_canonical_url(request))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
tags << tag(:meta, property: "og:type", content: page.og_type) if page.og_type.present?
|
|
49
|
+
|
|
50
|
+
if site_name.present?
|
|
51
|
+
tags << tag(:meta, property: "og:site_name", content: site_name)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Twitter Card tags
|
|
55
|
+
tags << tag(:meta, name: "twitter:card", content: page.twitter_card) if page.twitter_card.present?
|
|
56
|
+
tags << tag(:meta, name: "twitter:title", content: title_text)
|
|
57
|
+
|
|
58
|
+
if page.meta_description.present?
|
|
59
|
+
tags << tag(:meta, name: "twitter:description", content: page.meta_description)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if page.og_image_url.present?
|
|
63
|
+
tags << tag(:meta, name: "twitter:image", content: page.og_image_url)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
safe_join(tags, "\n")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
module ThemeHelper
|
|
5
|
+
def theme_stylesheet_tag
|
|
6
|
+
theme = NotPressed::ThemeRegistry.active
|
|
7
|
+
return unless theme&.theme_stylesheet
|
|
8
|
+
|
|
9
|
+
stylesheet_link_tag theme.theme_stylesheet, media: "all"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def theme_color_overrides
|
|
13
|
+
theme = NotPressed::ThemeRegistry.active
|
|
14
|
+
return unless theme
|
|
15
|
+
|
|
16
|
+
colors = theme.theme_color_scheme
|
|
17
|
+
return if colors.empty?
|
|
18
|
+
|
|
19
|
+
overrides = colors.map do |color|
|
|
20
|
+
setting_key = "theme.#{theme.plugin_name}.color_#{color[:key]}"
|
|
21
|
+
value = begin
|
|
22
|
+
NotPressed::Setting.get(setting_key)
|
|
23
|
+
rescue ActiveRecord::StatementInvalid, ActiveRecord::NoDatabaseError
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
value ||= color[:default]
|
|
27
|
+
" --np-color-#{color[:key]}: #{value};"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
tag.style(":root {\n#{overrides.join("\n")}\n}".html_safe)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def active_theme_class
|
|
34
|
+
theme = NotPressed::ThemeRegistry.active
|
|
35
|
+
if theme
|
|
36
|
+
"np-theme-#{theme.plugin_name.to_s.parameterize}"
|
|
37
|
+
else
|
|
38
|
+
"np-theme-default"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class FormMailer < ApplicationMailer
|
|
5
|
+
def submission_notification(form_submission)
|
|
6
|
+
@form_submission = form_submission
|
|
7
|
+
@form = form_submission.form
|
|
8
|
+
|
|
9
|
+
mail(
|
|
10
|
+
to: @form.email_recipient,
|
|
11
|
+
subject: "New submission: #{@form.name}"
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
module Sluggable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
validates :slug, presence: true,
|
|
9
|
+
uniqueness: true,
|
|
10
|
+
format: { with: /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/, message: "must be lowercase alphanumeric with hyphens" }
|
|
11
|
+
|
|
12
|
+
before_validation :generate_slug, on: :create
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def generate_slug
|
|
18
|
+
return if slug.present?
|
|
19
|
+
|
|
20
|
+
source = slug_source_attribute
|
|
21
|
+
return if source.blank?
|
|
22
|
+
|
|
23
|
+
base_slug = source.encode("UTF-8").parameterize
|
|
24
|
+
candidate = base_slug
|
|
25
|
+
counter = 2
|
|
26
|
+
|
|
27
|
+
while self.class.where(slug: candidate).exists?
|
|
28
|
+
candidate = "#{base_slug}-#{counter}"
|
|
29
|
+
counter += 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
self.slug = candidate
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def slug_source_attribute
|
|
36
|
+
if respond_to?(:title)
|
|
37
|
+
title
|
|
38
|
+
elsif respond_to?(:name)
|
|
39
|
+
name
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class Category < ActiveRecord::Base
|
|
5
|
+
self.table_name = "not_pressed_categories"
|
|
6
|
+
|
|
7
|
+
include NotPressed::Sluggable
|
|
8
|
+
|
|
9
|
+
has_many :pages, class_name: "NotPressed::Page", foreign_key: :category_id,
|
|
10
|
+
dependent: :nullify, inverse_of: :category
|
|
11
|
+
|
|
12
|
+
validates :name, presence: true, uniqueness: true
|
|
13
|
+
|
|
14
|
+
scope :ordered, -> { order(:name) }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class ContentBlock < ActiveRecord::Base
|
|
5
|
+
self.table_name = "not_pressed_content_blocks"
|
|
6
|
+
|
|
7
|
+
BUILT_IN_TYPES = %w[text heading image video code quote divider html form].freeze
|
|
8
|
+
|
|
9
|
+
mattr_accessor :custom_types, default: []
|
|
10
|
+
|
|
11
|
+
belongs_to :page, class_name: "NotPressed::Page", inverse_of: :content_blocks
|
|
12
|
+
|
|
13
|
+
validates :block_type, presence: true,
|
|
14
|
+
inclusion: { in: ->(_) { NotPressed::ContentBlock.registered_types } }
|
|
15
|
+
validates :position, presence: true,
|
|
16
|
+
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
|
17
|
+
|
|
18
|
+
scope :ordered, -> { order(position: :asc) }
|
|
19
|
+
|
|
20
|
+
def self.register_type(type_name)
|
|
21
|
+
custom_types << type_name.to_s unless custom_types.include?(type_name.to_s)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.registered_types
|
|
25
|
+
BUILT_IN_TYPES + custom_types
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.default_content_for(block_type)
|
|
29
|
+
custom = NotPressed::Rendering::RendererRegistry.default_content(block_type)
|
|
30
|
+
return custom if custom
|
|
31
|
+
|
|
32
|
+
case block_type.to_s
|
|
33
|
+
when "text" then { "body" => "" }
|
|
34
|
+
when "heading" then { "text" => "", "level" => 2 }
|
|
35
|
+
when "image" then { "url" => "", "alt" => "", "caption" => "" }
|
|
36
|
+
when "video" then { "url" => "", "caption" => "" }
|
|
37
|
+
when "code" then { "code" => "", "language" => "" }
|
|
38
|
+
when "quote" then { "text" => "", "attribution" => "" }
|
|
39
|
+
when "divider" then {}
|
|
40
|
+
when "html" then { "markup" => "" }
|
|
41
|
+
when "form" then { "form_id" => nil }
|
|
42
|
+
else {}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class Form < ActiveRecord::Base
|
|
5
|
+
self.table_name = "not_pressed_forms"
|
|
6
|
+
|
|
7
|
+
has_many :form_fields, class_name: "NotPressed::FormField",
|
|
8
|
+
foreign_key: :form_id,
|
|
9
|
+
dependent: :destroy,
|
|
10
|
+
inverse_of: :form do
|
|
11
|
+
def ordered
|
|
12
|
+
order(:position)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
accepts_nested_attributes_for :form_fields, allow_destroy: true
|
|
16
|
+
has_many :form_submissions, class_name: "NotPressed::FormSubmission",
|
|
17
|
+
foreign_key: :form_id,
|
|
18
|
+
dependent: :destroy,
|
|
19
|
+
inverse_of: :form
|
|
20
|
+
|
|
21
|
+
# Hook system callbacks
|
|
22
|
+
after_create { NotPressed::Hooks.fire(:after_form_create, self) }
|
|
23
|
+
after_save { NotPressed::Hooks.fire(:after_form_save, self) }
|
|
24
|
+
|
|
25
|
+
enum :status, { draft: 0, active: 1, archived: 2 }, prefix: true
|
|
26
|
+
|
|
27
|
+
validates :name, presence: true
|
|
28
|
+
validates :slug, presence: true,
|
|
29
|
+
uniqueness: true,
|
|
30
|
+
format: { with: /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/, message: "must be lowercase alphanumeric with hyphens" }
|
|
31
|
+
|
|
32
|
+
before_validation :generate_slug, on: :create
|
|
33
|
+
|
|
34
|
+
scope :active, -> { where(status: :active) }
|
|
35
|
+
scope :ordered, -> { order(:name) }
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def generate_slug
|
|
40
|
+
return if slug.present?
|
|
41
|
+
return if name.blank?
|
|
42
|
+
|
|
43
|
+
base_slug = name.encode("UTF-8").parameterize
|
|
44
|
+
candidate = base_slug
|
|
45
|
+
counter = 2
|
|
46
|
+
|
|
47
|
+
while self.class.where(slug: candidate).exists?
|
|
48
|
+
candidate = "#{base_slug}-#{counter}"
|
|
49
|
+
counter += 1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
self.slug = candidate
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class FormField < ActiveRecord::Base
|
|
5
|
+
self.table_name = "not_pressed_form_fields"
|
|
6
|
+
|
|
7
|
+
belongs_to :form, class_name: "NotPressed::Form", counter_cache: false, inverse_of: :form_fields
|
|
8
|
+
|
|
9
|
+
enum :field_type, {
|
|
10
|
+
text: 0, email: 1, textarea: 2, select: 3, checkbox: 4,
|
|
11
|
+
radio: 5, number: 6, phone: 7, url: 8, date: 9, file_upload: 10
|
|
12
|
+
}, prefix: true
|
|
13
|
+
|
|
14
|
+
validates :label, presence: true
|
|
15
|
+
validates :field_type, presence: true
|
|
16
|
+
|
|
17
|
+
scope :ordered, -> { order(:position) }
|
|
18
|
+
|
|
19
|
+
def has_options?
|
|
20
|
+
field_type_select? || field_type_radio? || field_type_checkbox?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class FormSubmission < ActiveRecord::Base
|
|
5
|
+
self.table_name = "not_pressed_form_submissions"
|
|
6
|
+
|
|
7
|
+
belongs_to :form, class_name: "NotPressed::Form", counter_cache: :submissions_count, inverse_of: :form_submissions
|
|
8
|
+
|
|
9
|
+
# Hook system callbacks
|
|
10
|
+
after_create { NotPressed::Hooks.fire(:after_form_submit, self.form, self) }
|
|
11
|
+
|
|
12
|
+
validates :data, presence: true
|
|
13
|
+
validates :submitted_at, presence: true
|
|
14
|
+
|
|
15
|
+
before_validation -> { self.submitted_at ||= Time.current }, on: :create
|
|
16
|
+
|
|
17
|
+
scope :recent, -> { order(submitted_at: :desc) }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class MediaAttachment < ActiveRecord::Base
|
|
5
|
+
self.table_name = "not_pressed_media_attachments"
|
|
6
|
+
|
|
7
|
+
IMAGE_CONTENT_TYPES = %w[image/jpeg image/png image/gif image/webp image/svg+xml].freeze
|
|
8
|
+
|
|
9
|
+
has_one_attached :file
|
|
10
|
+
|
|
11
|
+
# Hook system callbacks
|
|
12
|
+
after_create { NotPressed::Hooks.fire(:after_media_upload, self) }
|
|
13
|
+
after_destroy { NotPressed::Hooks.fire(:after_media_destroy, self) }
|
|
14
|
+
|
|
15
|
+
validate :file_attached, on: :create
|
|
16
|
+
|
|
17
|
+
before_save :extract_file_metadata, if: -> { file.attached? }
|
|
18
|
+
|
|
19
|
+
scope :images, -> { where(content_type: IMAGE_CONTENT_TYPES) }
|
|
20
|
+
scope :documents, -> { where.not(content_type: IMAGE_CONTENT_TYPES) }
|
|
21
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
22
|
+
|
|
23
|
+
def image?
|
|
24
|
+
content_type.in?(IMAGE_CONTENT_TYPES)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def thumbnail
|
|
28
|
+
return nil unless image?
|
|
29
|
+
|
|
30
|
+
file.variant(resize_to_limit: [200, 200])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def small
|
|
34
|
+
return nil unless image?
|
|
35
|
+
|
|
36
|
+
file.variant(resize_to_limit: [400, 400])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def medium
|
|
40
|
+
return nil unless image?
|
|
41
|
+
|
|
42
|
+
file.variant(resize_to_limit: [800, 800])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def large
|
|
46
|
+
return nil unless image?
|
|
47
|
+
|
|
48
|
+
file.variant(resize_to_limit: [1200, 1200])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def hero
|
|
52
|
+
return nil unless image?
|
|
53
|
+
|
|
54
|
+
file.variant(resize_to_limit: [1920, 1080])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def file_attached
|
|
60
|
+
errors.add(:file, "must be attached") unless file.attached?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def extract_file_metadata
|
|
64
|
+
self.content_type = file.blob.content_type
|
|
65
|
+
self.file_size = file.blob.byte_size
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|