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,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class Page < ActiveRecord::Base
|
|
5
|
+
self.table_name = "not_pressed_pages"
|
|
6
|
+
|
|
7
|
+
belongs_to :parent, class_name: "NotPressed::Page", optional: true, inverse_of: :children
|
|
8
|
+
has_many :children, class_name: "NotPressed::Page", foreign_key: :parent_id,
|
|
9
|
+
dependent: :destroy, inverse_of: :parent
|
|
10
|
+
has_many :content_blocks, class_name: "NotPressed::ContentBlock",
|
|
11
|
+
dependent: :destroy, inverse_of: :page
|
|
12
|
+
has_many :page_versions, class_name: "NotPressed::PageVersion",
|
|
13
|
+
dependent: :destroy
|
|
14
|
+
belongs_to :category, class_name: "NotPressed::Category", optional: true, inverse_of: :pages
|
|
15
|
+
has_many :taggings, class_name: "NotPressed::Tagging", foreign_key: :page_id,
|
|
16
|
+
dependent: :destroy, inverse_of: :page
|
|
17
|
+
has_many :tags, through: :taggings
|
|
18
|
+
|
|
19
|
+
attr_accessor :skip_versioning, :page_password
|
|
20
|
+
|
|
21
|
+
# Hook system callbacks
|
|
22
|
+
after_create { NotPressed::Hooks.fire(:after_page_create, self) }
|
|
23
|
+
before_save :fire_before_page_hooks
|
|
24
|
+
after_save :fire_after_page_hooks
|
|
25
|
+
after_destroy { NotPressed::Hooks.fire(:after_page_destroy, self) }
|
|
26
|
+
|
|
27
|
+
enum :status, { draft: 0, published: 1, archived: 2, scheduled: 3 }
|
|
28
|
+
enum :visibility, { public_page: 0, unlisted: 1, password_protected: 2 }, prefix: :visibility
|
|
29
|
+
|
|
30
|
+
validates :title, presence: true
|
|
31
|
+
validates :content_type, inclusion: { in: ->(_) { NotPressed::ContentTypeRegistry.names } }
|
|
32
|
+
validates :slug, presence: true,
|
|
33
|
+
uniqueness: true,
|
|
34
|
+
format: { with: /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/, message: "must be lowercase alphanumeric with hyphens" }
|
|
35
|
+
validates :scheduled_at, presence: true, if: :scheduled?
|
|
36
|
+
validates :layout, inclusion: { in: ->(_) { NotPressed.configuration.available_layouts } }, allow_nil: true
|
|
37
|
+
validates :password_digest, presence: true, if: :visibility_password_protected?
|
|
38
|
+
validate :scheduled_at_must_be_future, if: :scheduled?
|
|
39
|
+
validate :scheduled_at_must_be_nil_unless_scheduled
|
|
40
|
+
|
|
41
|
+
before_validation :generate_slug, on: :create
|
|
42
|
+
before_validation :hash_page_password
|
|
43
|
+
before_save :set_published_at
|
|
44
|
+
after_save :create_version_snapshot, unless: :skip_versioning
|
|
45
|
+
after_save :invalidate_navigation_cache
|
|
46
|
+
after_destroy :invalidate_navigation_cache
|
|
47
|
+
|
|
48
|
+
scope :of_type, ->(type) { where(content_type: type) }
|
|
49
|
+
scope :roots, -> { where(parent_id: nil) }
|
|
50
|
+
scope :ordered, -> { order(position: :asc) }
|
|
51
|
+
scope :visible, -> { where(status: :published) }
|
|
52
|
+
scope :publicly_accessible, -> { where(visibility: [:public_page, :unlisted]) }
|
|
53
|
+
scope :needs_publishing, -> { where(status: :scheduled).where("scheduled_at <= ?", Time.current) }
|
|
54
|
+
scope :blog_posts, -> { of_type("blog_post") }
|
|
55
|
+
|
|
56
|
+
def authenticate_page_password(password)
|
|
57
|
+
return false if password_digest.blank?
|
|
58
|
+
|
|
59
|
+
salt, stored_hash = password_digest.split(":", 2)
|
|
60
|
+
Digest::SHA256.hexdigest("#{salt}:#{password}") == stored_hash
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def meta_title_or_title
|
|
64
|
+
meta_title.presence || title
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def effective_canonical_url(request)
|
|
68
|
+
canonical_url.presence || request.original_url
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def meta_robots_tag
|
|
72
|
+
return meta_robots if meta_robots.present?
|
|
73
|
+
return "noindex" if draft? || archived?
|
|
74
|
+
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.publish_scheduled!
|
|
79
|
+
needs_publishing.find_each do |page|
|
|
80
|
+
page.update!(status: :published, scheduled_at: nil)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def fire_before_page_hooks
|
|
87
|
+
NotPressed::Hooks.fire(:before_page_save, self)
|
|
88
|
+
if status_changed? && published?
|
|
89
|
+
@_publishing = true
|
|
90
|
+
NotPressed::Hooks.fire(:before_page_publish, self)
|
|
91
|
+
else
|
|
92
|
+
@_publishing = false
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def fire_after_page_hooks
|
|
97
|
+
NotPressed::Hooks.fire(:after_page_save, self)
|
|
98
|
+
if @_publishing
|
|
99
|
+
NotPressed::Hooks.fire(:after_page_publish, self)
|
|
100
|
+
@_publishing = false
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def hash_page_password
|
|
105
|
+
if page_password.present?
|
|
106
|
+
salt = SecureRandom.hex(16)
|
|
107
|
+
hash = Digest::SHA256.hexdigest("#{salt}:#{page_password}")
|
|
108
|
+
self.password_digest = "#{salt}:#{hash}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def set_published_at
|
|
113
|
+
if status_changed? && published?
|
|
114
|
+
self.published_at ||= Time.current
|
|
115
|
+
elsif status_changed? && status_was == "published"
|
|
116
|
+
self.published_at = nil
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def create_version_snapshot
|
|
121
|
+
event = if previously_new_record?
|
|
122
|
+
"created"
|
|
123
|
+
elsif saved_change_to_status?
|
|
124
|
+
status
|
|
125
|
+
elsif saved_change_to_title? || saved_change_to_slug?
|
|
126
|
+
"updated"
|
|
127
|
+
else
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
snapshot = {
|
|
132
|
+
title: title,
|
|
133
|
+
slug: slug,
|
|
134
|
+
status: status,
|
|
135
|
+
meta_description: meta_description,
|
|
136
|
+
content_type: content_type,
|
|
137
|
+
published_at: published_at,
|
|
138
|
+
blocks: content_blocks.ordered.map { |b| { block_type: b.block_type, content: b.content, position: b.position } }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
page_versions.create!(
|
|
142
|
+
version_number: (page_versions.maximum(:version_number).to_i + 1),
|
|
143
|
+
snapshot: snapshot,
|
|
144
|
+
event: event
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def invalidate_navigation_cache
|
|
149
|
+
NotPressed.invalidate_navigation!
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def generate_slug
|
|
153
|
+
return if slug.present?
|
|
154
|
+
return if title.blank?
|
|
155
|
+
|
|
156
|
+
base_slug = title.encode("UTF-8").parameterize
|
|
157
|
+
candidate = base_slug
|
|
158
|
+
counter = 2
|
|
159
|
+
|
|
160
|
+
while self.class.where(slug: candidate).exists?
|
|
161
|
+
candidate = "#{base_slug}-#{counter}"
|
|
162
|
+
counter += 1
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
self.slug = candidate
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def scheduled_at_must_be_future
|
|
169
|
+
return if scheduled_at.blank?
|
|
170
|
+
|
|
171
|
+
if scheduled_at <= Time.current
|
|
172
|
+
errors.add(:scheduled_at, "must be in the future")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def scheduled_at_must_be_nil_unless_scheduled
|
|
177
|
+
return if scheduled? || scheduled_at.blank?
|
|
178
|
+
|
|
179
|
+
errors.add(:scheduled_at, "must be blank unless status is scheduled")
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class PageVersion < ActiveRecord::Base
|
|
5
|
+
self.table_name = "not_pressed_page_versions"
|
|
6
|
+
|
|
7
|
+
belongs_to :page, class_name: "NotPressed::Page"
|
|
8
|
+
|
|
9
|
+
validates :version_number, presence: true
|
|
10
|
+
validates :snapshot, presence: true
|
|
11
|
+
validates :event, presence: true
|
|
12
|
+
|
|
13
|
+
scope :ordered, -> { order(version_number: :desc) }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class Setting < ActiveRecord::Base
|
|
5
|
+
self.table_name = "not_pressed_settings"
|
|
6
|
+
|
|
7
|
+
validates :key, presence: true, uniqueness: true
|
|
8
|
+
|
|
9
|
+
def self.get(key)
|
|
10
|
+
find_by(key: key)&.value
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.set(key, value)
|
|
14
|
+
record = find_or_initialize_by(key: key)
|
|
15
|
+
record.value = value
|
|
16
|
+
record.save!
|
|
17
|
+
record
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class Tag < ActiveRecord::Base
|
|
5
|
+
self.table_name = "not_pressed_tags"
|
|
6
|
+
|
|
7
|
+
include NotPressed::Sluggable
|
|
8
|
+
|
|
9
|
+
has_many :taggings, class_name: "NotPressed::Tagging", foreign_key: :tag_id,
|
|
10
|
+
dependent: :destroy, inverse_of: :tag
|
|
11
|
+
has_many :pages, through: :taggings
|
|
12
|
+
|
|
13
|
+
validates :name, presence: true, uniqueness: true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class Tagging < ActiveRecord::Base
|
|
5
|
+
self.table_name = "not_pressed_taggings"
|
|
6
|
+
|
|
7
|
+
belongs_to :tag, class_name: "NotPressed::Tag", inverse_of: :taggings
|
|
8
|
+
belongs_to :page, class_name: "NotPressed::Page", inverse_of: :taggings
|
|
9
|
+
|
|
10
|
+
validates :tag_id, uniqueness: { scope: :page_id }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class AnalyticsPlugin < Plugin
|
|
5
|
+
name "Analytics"
|
|
6
|
+
version "1.0.0"
|
|
7
|
+
description "Add analytics tracking to your site"
|
|
8
|
+
author "NotPressed"
|
|
9
|
+
|
|
10
|
+
settings do
|
|
11
|
+
string :tracking_id, label: "Tracking ID", required: true
|
|
12
|
+
select :provider, label: "Provider", options: %w[google_analytics plausible umami], default: "google_analytics"
|
|
13
|
+
boolean :anonymize_ip, label: "Anonymize IP", default: true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def activate!
|
|
17
|
+
plugin_instance = self
|
|
18
|
+
|
|
19
|
+
@page_content_filter = proc do |html, _page|
|
|
20
|
+
plugin_instance.inject_analytics(html)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@after_publish_hook = proc do |page|
|
|
24
|
+
Rails.logger.info "[Analytics] Page published: #{page.respond_to?(:title) ? page.title : page}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
NotPressed::Hooks.register_filter(:page_content, &@page_content_filter)
|
|
28
|
+
NotPressed::Hooks.register(:after_page_publish, &@after_publish_hook)
|
|
29
|
+
|
|
30
|
+
self.class.plugin_block_types.each do |bt|
|
|
31
|
+
NotPressed::ContentBlock.register_type(bt[:name].to_s)
|
|
32
|
+
end
|
|
33
|
+
self.class.plugin_admin_menus.each do |menu|
|
|
34
|
+
NotPressed::Admin::MenuRegistry.register(
|
|
35
|
+
menu[:label],
|
|
36
|
+
path: menu[:path],
|
|
37
|
+
icon: menu[:icon],
|
|
38
|
+
section: :main,
|
|
39
|
+
position: 100
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@active = true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def deactivate!
|
|
47
|
+
NotPressed::Hooks.unregister_filter(:page_content, @page_content_filter) if @page_content_filter
|
|
48
|
+
NotPressed::Hooks.unregister(:after_page_publish, @after_publish_hook) if @after_publish_hook
|
|
49
|
+
|
|
50
|
+
self.class.plugin_block_types.each do |bt|
|
|
51
|
+
NotPressed::ContentBlock.custom_types.delete(bt[:name].to_s)
|
|
52
|
+
end
|
|
53
|
+
self.class.plugin_admin_menus.each do |menu|
|
|
54
|
+
NotPressed::Admin::MenuRegistry.unregister(menu[:label], path: menu[:path])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@active = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def inject_analytics(html)
|
|
61
|
+
tracking_id = get_setting(:tracking_id)
|
|
62
|
+
return html if tracking_id.nil? || tracking_id.to_s.strip.empty?
|
|
63
|
+
|
|
64
|
+
provider = get_setting(:provider) || "google_analytics"
|
|
65
|
+
script = analytics_script(provider, tracking_id)
|
|
66
|
+
"#{html}#{script}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def analytics_script(provider, tracking_id)
|
|
72
|
+
case provider.to_s
|
|
73
|
+
when "google_analytics"
|
|
74
|
+
google_analytics_script(tracking_id)
|
|
75
|
+
when "plausible"
|
|
76
|
+
plausible_script(tracking_id)
|
|
77
|
+
when "umami"
|
|
78
|
+
umami_script(tracking_id)
|
|
79
|
+
else
|
|
80
|
+
""
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def google_analytics_script(tracking_id)
|
|
85
|
+
anonymize = get_setting(:anonymize_ip)
|
|
86
|
+
anonymize_config = anonymize.to_s == "true" ? ", { 'anonymize_ip': true }" : ""
|
|
87
|
+
<<~HTML
|
|
88
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=#{ERB::Util.html_escape(tracking_id)}"></script>
|
|
89
|
+
<script>
|
|
90
|
+
window.dataLayer = window.dataLayer || [];
|
|
91
|
+
function gtag(){dataLayer.push(arguments);}
|
|
92
|
+
gtag('js', new Date());
|
|
93
|
+
gtag('config', '#{ERB::Util.html_escape(tracking_id)}'#{anonymize_config});
|
|
94
|
+
</script>
|
|
95
|
+
HTML
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def plausible_script(tracking_id)
|
|
99
|
+
%(<script defer data-domain="#{ERB::Util.html_escape(tracking_id)}" src="https://plausible.io/js/script.js"></script>\n)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def umami_script(tracking_id)
|
|
103
|
+
%(<script async src="https://analytics.umami.is/script.js" data-website-id="#{ERB::Util.html_escape(tracking_id)}"></script>\n)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotPressed
|
|
4
|
+
class CalloutBlockPlugin < Plugin
|
|
5
|
+
name "Callout Block"
|
|
6
|
+
version "1.0.0"
|
|
7
|
+
description "Adds a styled callout/alert block type"
|
|
8
|
+
author "NotPressed"
|
|
9
|
+
|
|
10
|
+
block_type :callout, label: "Callout", icon: "megaphone"
|
|
11
|
+
|
|
12
|
+
STYLES = %w[info warning success danger].freeze
|
|
13
|
+
|
|
14
|
+
DEFAULT_CONTENT = { "title" => "", "text" => "", "style" => "info" }.freeze
|
|
15
|
+
|
|
16
|
+
CALLOUT_RENDERER = lambda do |content, _block|
|
|
17
|
+
style = STYLES.include?(content["style"].to_s) ? content["style"].to_s : "info"
|
|
18
|
+
title = ERB::Util.html_escape(content["title"].to_s)
|
|
19
|
+
text = ERB::Util.html_escape(content["text"].to_s)
|
|
20
|
+
|
|
21
|
+
<<~HTML.strip
|
|
22
|
+
<div class="np-content-callout np-callout-#{style}">
|
|
23
|
+
<div class="np-callout-title">#{title}</div>
|
|
24
|
+
<div class="np-callout-text">#{text}</div>
|
|
25
|
+
</div>
|
|
26
|
+
HTML
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def activate!
|
|
30
|
+
super
|
|
31
|
+
|
|
32
|
+
NotPressed::Rendering::RendererRegistry.register("callout", CALLOUT_RENDERER)
|
|
33
|
+
NotPressed::Rendering::RendererRegistry.register_editor("callout", "not_pressed/admin/content_blocks/editors/callout")
|
|
34
|
+
NotPressed::Rendering::RendererRegistry.register_default_content("callout", DEFAULT_CONTENT.dup)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def deactivate!
|
|
38
|
+
super
|
|
39
|
+
|
|
40
|
+
NotPressed::Rendering::RendererRegistry.unregister("callout")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class NotPressed::StarterTheme < NotPressed::Theme
|
|
4
|
+
name "Starter"
|
|
5
|
+
version "1.0.0"
|
|
6
|
+
description "A clean, minimal theme for NotPressed. Responsive layouts with customizable colors."
|
|
7
|
+
author "NotPressed"
|
|
8
|
+
|
|
9
|
+
layout :default, label: "Default", primary: true
|
|
10
|
+
layout :full_width, label: "Full Width"
|
|
11
|
+
layout :sidebar, label: "With Sidebar"
|
|
12
|
+
|
|
13
|
+
stylesheet "not_pressed/themes/starter.css"
|
|
14
|
+
templates_path File.expand_path("../../../views/themes/starter", __dir__)
|
|
15
|
+
|
|
16
|
+
color_scheme do
|
|
17
|
+
color :primary, "#4f46e5", label: "Primary"
|
|
18
|
+
color :secondary, "#7c3aed", label: "Secondary"
|
|
19
|
+
color :background, "#ffffff", label: "Background"
|
|
20
|
+
color :surface, "#f8fafc", label: "Surface"
|
|
21
|
+
color :text, "#1e293b", label: "Text"
|
|
22
|
+
color :text_muted, "#64748b", label: "Muted Text"
|
|
23
|
+
color :accent, "#06b6d4", label: "Accent"
|
|
24
|
+
color :border, "#e2e8f0", label: "Border"
|
|
25
|
+
end
|
|
26
|
+
end
|