lean_cms 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +235 -0
  3. data/LICENSE +21 -0
  4. data/README.md +107 -0
  5. data/app/assets/images/lean_cms/sloth-404.png +0 -0
  6. data/app/assets/images/lean_cms/sloth-500.png +0 -0
  7. data/app/assets/images/lean_cms/sloth-favicon-16.png +0 -0
  8. data/app/assets/images/lean_cms/sloth-favicon-32.png +0 -0
  9. data/app/assets/images/lean_cms/sloth-favicon-64.png +0 -0
  10. data/app/assets/images/lean_cms/sloth-logo.png +0 -0
  11. data/app/assets/lean_cms/actiontext.css +440 -0
  12. data/app/assets/lean_cms/cms_edit_controls.css +548 -0
  13. data/app/assets/tailwind/lean_cms/engine.css +14 -0
  14. data/app/components/lean_cms/base_component.rb +61 -0
  15. data/app/components/lean_cms/bullets_section_component.html.erb +23 -0
  16. data/app/components/lean_cms/bullets_section_component.rb +54 -0
  17. data/app/components/lean_cms/cards_section_component.html.erb +237 -0
  18. data/app/components/lean_cms/cards_section_component.rb +71 -0
  19. data/app/components/lean_cms/editable_content_component.html.erb +15 -0
  20. data/app/components/lean_cms/editable_content_component.rb +53 -0
  21. data/app/components/lean_cms/section_component.html.erb +18 -0
  22. data/app/components/lean_cms/section_component.rb +35 -0
  23. data/app/controllers/concerns/lean_cms/authentication.rb +60 -0
  24. data/app/controllers/concerns/lean_cms/authorization.rb +60 -0
  25. data/app/controllers/lean_cms/activity_controller.rb +16 -0
  26. data/app/controllers/lean_cms/application_controller.rb +48 -0
  27. data/app/controllers/lean_cms/dashboard_controller.rb +13 -0
  28. data/app/controllers/lean_cms/form_submissions_controller.rb +37 -0
  29. data/app/controllers/lean_cms/notification_settings_controller.rb +145 -0
  30. data/app/controllers/lean_cms/notifications_controller.rb +26 -0
  31. data/app/controllers/lean_cms/page_contents_controller.rb +403 -0
  32. data/app/controllers/lean_cms/password_setup_controller.rb +65 -0
  33. data/app/controllers/lean_cms/passwords_controller.rb +42 -0
  34. data/app/controllers/lean_cms/posts_controller.rb +78 -0
  35. data/app/controllers/lean_cms/sessions_controller.rb +50 -0
  36. data/app/controllers/lean_cms/settings_controller.rb +124 -0
  37. data/app/controllers/lean_cms/users_controller.rb +113 -0
  38. data/app/helpers/lean_cms/activity_helper.rb +190 -0
  39. data/app/helpers/lean_cms/application_helper.rb +43 -0
  40. data/app/helpers/lean_cms/content_helper.rb +34 -0
  41. data/app/helpers/lean_cms/page_content_helper.rb +359 -0
  42. data/app/javascript/controllers/cards_editor_controller.js +317 -0
  43. data/app/javascript/controllers/cms_sticky_overlay_controller.js +59 -0
  44. data/app/javascript/controllers/field_editor_form_controller.js +68 -0
  45. data/app/javascript/controllers/field_editor_modal_controller.js +79 -0
  46. data/app/javascript/controllers/inline_edit_controller.js +414 -0
  47. data/app/javascript/controllers/inline_edit_toggle_controller.js +81 -0
  48. data/app/javascript/controllers/notifications_controller.js +19 -0
  49. data/app/javascript/controllers/settings_inline_edit_sync_controller.js +38 -0
  50. data/app/javascript/controllers/settings_override_controller.js +45 -0
  51. data/app/mailers/lean_cms/application_mailer.rb +6 -0
  52. data/app/mailers/lean_cms/passwords_mailer.rb +8 -0
  53. data/app/mailers/lean_cms/users_mailer.rb +39 -0
  54. data/app/models/lean_cms/current.rb +6 -0
  55. data/app/models/lean_cms/form_submission.rb +45 -0
  56. data/app/models/lean_cms/magic_link.rb +76 -0
  57. data/app/models/lean_cms/meta_tag.rb +30 -0
  58. data/app/models/lean_cms/notification_setting.rb +69 -0
  59. data/app/models/lean_cms/page.rb +23 -0
  60. data/app/models/lean_cms/page_content.rb +245 -0
  61. data/app/models/lean_cms/post.rb +65 -0
  62. data/app/models/lean_cms/session.rb +7 -0
  63. data/app/models/lean_cms/setting.rb +156 -0
  64. data/app/policies/lean_cms/application_policy.rb +35 -0
  65. data/app/policies/lean_cms/page_content_policy.rb +31 -0
  66. data/app/policies/lean_cms/post_policy.rb +37 -0
  67. data/app/policies/lean_cms/setting_policy.rb +17 -0
  68. data/app/views/layouts/lean_cms/application.html.erb +114 -0
  69. data/app/views/layouts/lean_cms/auth.html.erb +200 -0
  70. data/app/views/lean_cms/activity/index.html.erb +79 -0
  71. data/app/views/lean_cms/dashboard/index.html.erb +180 -0
  72. data/app/views/lean_cms/form_submissions/index.html.erb +104 -0
  73. data/app/views/lean_cms/form_submissions/show.html.erb +157 -0
  74. data/app/views/lean_cms/notification_settings/edit.html.erb +192 -0
  75. data/app/views/lean_cms/notifications/index.html.erb +72 -0
  76. data/app/views/lean_cms/notifications/show.html.erb +39 -0
  77. data/app/views/lean_cms/page_contents/_field_editor.html.erb +174 -0
  78. data/app/views/lean_cms/page_contents/edit.html.erb +428 -0
  79. data/app/views/lean_cms/page_contents/index.html.erb +113 -0
  80. data/app/views/lean_cms/password_setup/show.html.erb +35 -0
  81. data/app/views/lean_cms/passwords/edit.html.erb +26 -0
  82. data/app/views/lean_cms/passwords/new.html.erb +21 -0
  83. data/app/views/lean_cms/passwords_mailer/reset.html.erb +6 -0
  84. data/app/views/lean_cms/passwords_mailer/reset.text.erb +4 -0
  85. data/app/views/lean_cms/posts/_form.html.erb +118 -0
  86. data/app/views/lean_cms/posts/edit.html.erb +31 -0
  87. data/app/views/lean_cms/posts/index.html.erb +100 -0
  88. data/app/views/lean_cms/posts/new.html.erb +16 -0
  89. data/app/views/lean_cms/sessions/new.html.erb +28 -0
  90. data/app/views/lean_cms/settings/edit.html.erb +384 -0
  91. data/app/views/lean_cms/shared/_admin_bar.html.erb +85 -0
  92. data/app/views/lean_cms/shared/_header.html.erb +86 -0
  93. data/app/views/lean_cms/shared/_notifications_bell.html.erb +84 -0
  94. data/app/views/lean_cms/shared/_sidebar.html.erb +102 -0
  95. data/app/views/lean_cms/users/_form.html.erb +105 -0
  96. data/app/views/lean_cms/users/edit.html.erb +8 -0
  97. data/app/views/lean_cms/users/index.html.erb +99 -0
  98. data/app/views/lean_cms/users/new.html.erb +8 -0
  99. data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.html.erb +13 -0
  100. data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.text.erb +11 -0
  101. data/app/views/lean_cms/users_mailer/invitation.html.erb +13 -0
  102. data/app/views/lean_cms/users_mailer/invitation.text.erb +11 -0
  103. data/app/views/lean_cms/users_mailer/reactivation.html.erb +13 -0
  104. data/app/views/lean_cms/users_mailer/reactivation.text.erb +11 -0
  105. data/config/importmap.rb +8 -0
  106. data/config/routes.rb +78 -0
  107. data/db/migrate/20251112034030_create_lean_cms_tables.rb +131 -0
  108. data/db/migrate/20260513000001_create_lean_cms_auth_tables.rb +31 -0
  109. data/db/migrate/20260514000001_create_paper_trail_versions.rb +16 -0
  110. data/db/migrate/20260514000002_create_action_text_tables.rb +18 -0
  111. data/db/migrate/20260514000003_create_active_storage_tables.rb +45 -0
  112. data/db/migrate/20260514000004_create_noticed_tables.rb +27 -0
  113. data/lib/generators/lean_cms/demo/demo_generator.rb +54 -0
  114. data/lib/generators/lean_cms/demo/templates/lean_cms_structure.yml +129 -0
  115. data/lib/generators/lean_cms/demo/templates/pages_controller.rb +30 -0
  116. data/lib/generators/lean_cms/demo/templates/views/pages/about.html.erb +40 -0
  117. data/lib/generators/lean_cms/demo/templates/views/pages/contact.html.erb +55 -0
  118. data/lib/generators/lean_cms/demo/templates/views/pages/home.html.erb +31 -0
  119. data/lib/generators/lean_cms/install/install_generator.rb +317 -0
  120. data/lib/generators/lean_cms/install/templates/add_lean_cms_columns_to_users.rb.tt +7 -0
  121. data/lib/generators/lean_cms/install/templates/lean_cms.rb +11 -0
  122. data/lib/generators/lean_cms/install/templates/lean_cms_structure.yml +29 -0
  123. data/lib/lean_cms/configuration.rb +32 -0
  124. data/lib/lean_cms/engine.rb +93 -0
  125. data/lib/lean_cms/loader.rb +217 -0
  126. data/lib/lean_cms/sync_helper.rb +182 -0
  127. data/lib/lean_cms/version.rb +3 -0
  128. data/lib/lean_cms.rb +26 -0
  129. data/lib/tasks/lean_cms.rake +390 -0
  130. metadata +313 -0
@@ -0,0 +1,317 @@
1
+ require "rails/generators"
2
+ require "rails/generators/migration"
3
+
4
+ module LeanCms
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Installs Lean CMS into the host Rails application"
12
+
13
+ class_option :user_class, type: :string, default: "User",
14
+ desc: "Name of the user model class (default: User). " \
15
+ "Use --user=Admin or similar if your auth gem (Devise, etc.) uses a different name."
16
+
17
+ def self.next_migration_number(dirname)
18
+ next_migration_number = current_migration_number(dirname) + 1
19
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
20
+ end
21
+
22
+ # Stop before any side effects if the host doesn't have a user model
23
+ # with a matching table yet, or the existing table is missing the base
24
+ # columns (email_address, password_digest) Lean CMS hard-codes against.
25
+ def check_user_model
26
+ user_class = options[:user_class]
27
+ table_name = user_class.tableize
28
+
29
+ unless ActiveRecord::Base.connection.table_exists?(table_name)
30
+ say "\n#{"=" * 64}", :red
31
+ say "Lean CMS install can't continue.", :red
32
+ say "=" * 64, :red
33
+ say ""
34
+ say "No `#{table_name}` table found in your database."
35
+ say ""
36
+ say "Lean CMS expects an existing #{user_class} model. Set one up first:"
37
+ say ""
38
+ say " - Rails 8 built-in auth: bin/rails generate authentication"
39
+ say " - Devise / Clearance / etc.: install per that gem's instructions"
40
+ say " - Custom model: bin/rails generate model #{user_class} ...", :yellow
41
+ say ""
42
+ say "Then run bin/rails db:migrate and re-run this generator."
43
+ say ""
44
+ say "If your user model isn't named #{user_class}, pass --user=ClassName:"
45
+ say " bin/rails generate lean_cms:install --user=Admin"
46
+ say ""
47
+ exit 1
48
+ end
49
+
50
+ existing = ActiveRecord::Base.connection.columns(table_name).map(&:name)
51
+ required_base = %w[email_address password_digest]
52
+ missing_base = required_base - existing
53
+
54
+ return if missing_base.empty?
55
+
56
+ say "\n#{"=" * 64}", :red
57
+ say "Lean CMS install can't continue.", :red
58
+ say "=" * 64, :red
59
+ say ""
60
+ say "The `#{table_name}` table is missing required columns: #{missing_base.join(", ")}."
61
+ say ""
62
+ say "Lean CMS authenticates with `#{user_class}.authenticate_by(email_address:, password:)`,"
63
+ say "so both `email_address` and `password_digest` columns are required."
64
+ say ""
65
+ say "Add them yourself (rename `email` -> `email_address` if you have it, etc.) and re-run."
66
+ say ""
67
+ exit 1
68
+ end
69
+
70
+ # Detect Rails 8's built-in auth (`bin/rails generate authentication`)
71
+ # and warn — it conflicts with Lean CMS's Authentication concern.
72
+ # Both define `before_action :require_authentication`, and the
73
+ # last-included wins. Without manual cleanup, /lean-cms protected
74
+ # routes redirect to /session/new (Rails 8's) instead of /lean-cms/login.
75
+ def check_for_rails_auth_conflict
76
+ ac_path = File.join(destination_root, "app", "controllers", "application_controller.rb")
77
+ return unless File.exist?(ac_path)
78
+ return unless File.read(ac_path).match?(/^\s*include\s+Authentication\s*$/)
79
+
80
+ say "\n#{"=" * 64}", :yellow
81
+ say "WARNING: Rails 8 authentication detected.", :yellow
82
+ say "=" * 64, :yellow
83
+ say ""
84
+ say "app/controllers/application_controller.rb already includes Rails 8's"
85
+ say "built-in Authentication concern. That conflicts with Lean CMS auth:"
86
+ say "both define `before_action :require_authentication`, and the"
87
+ say "last-included one wins. As-is, /lean-cms admin routes will redirect"
88
+ say "to /session/new (Rails 8's login) instead of /lean-cms/login."
89
+ say ""
90
+ say "After this install completes, clean up Rails 8 auth so Lean CMS owns auth:"
91
+ say ""
92
+ say " 1. In app/controllers/application_controller.rb:"
93
+ say " Remove: include Authentication", :red
94
+ say " Keep: include LeanCms::Authentication", :green
95
+ say ""
96
+ say " 2. Delete the Rails-8-generated auth files (Lean CMS replaces them):"
97
+ say " app/controllers/sessions_controller.rb", :cyan
98
+ say " app/controllers/passwords_controller.rb", :cyan
99
+ say " app/controllers/concerns/authentication.rb", :cyan
100
+ say " app/models/session.rb", :cyan
101
+ say " app/models/current.rb", :cyan
102
+ say " app/views/sessions/", :cyan
103
+ say " app/views/passwords/", :cyan
104
+ say " app/views/passwords_mailer/", :cyan
105
+ say " app/mailers/passwords_mailer.rb", :cyan
106
+ say " test/controllers/sessions_controller_test.rb", :cyan
107
+ say " test/controllers/passwords_controller_test.rb", :cyan
108
+ say ""
109
+ say " 3. Remove the Rails 8 auth routes from config/routes.rb:"
110
+ say " resource :session", :red
111
+ say " resources :passwords, param: :token", :red
112
+ say ""
113
+ say "Lean CMS provides all of this functionality under /lean-cms/."
114
+ say "Continuing the install — you'll see this WARNING again at the end."
115
+ say "#{"=" * 64}", :yellow
116
+ say ""
117
+ end
118
+
119
+ # Generate a migration that adds the Lean CMS-specific columns the host
120
+ # user table doesn't already have (name, active, permission flags, …).
121
+ # Silently skips if everything's already in place. The migration runs
122
+ # as part of `db:migrate` in `run_migrations` below.
123
+ def add_missing_user_columns
124
+ user_class = options[:user_class]
125
+ @user_table = user_class.tableize
126
+ existing = ActiveRecord::Base.connection.columns(@user_table).map(&:name)
127
+
128
+ required = [
129
+ [:name, :string, {}],
130
+ [:active, :boolean, { default: true, null: false }],
131
+ [:must_change_password, :boolean, { default: false, null: false }],
132
+ [:last_login_at, :datetime, {}],
133
+ [:is_super_admin, :boolean, { default: false, null: false }],
134
+ [:can_edit_pages, :boolean, { default: false, null: false }],
135
+ [:can_edit_blog, :boolean, { default: false, null: false }],
136
+ [:can_manage_users, :boolean, { default: false, null: false }],
137
+ [:can_access_settings, :boolean, { default: false, null: false }]
138
+ ]
139
+
140
+ @missing_columns = required.reject { |name, _, _| existing.include?(name.to_s) }
141
+ return if @missing_columns.empty?
142
+
143
+ say "Generating migration to add Lean CMS columns to `#{@user_table}` " \
144
+ "(#{@missing_columns.map(&:first).join(", ")})...", :yellow
145
+ migration_template "add_lean_cms_columns_to_users.rb.tt",
146
+ "db/migrate/add_lean_cms_columns_to_#{@user_table}.rb"
147
+ end
148
+
149
+ def copy_initializer
150
+ @user_class = options[:user_class]
151
+ template "lean_cms.rb", "config/initializers/lean_cms.rb"
152
+ end
153
+
154
+ # Inject the engine's Tailwind sources into the host's Tailwind input file
155
+ # so utilities referenced from gem views/controllers actually get emitted.
156
+ # tailwindcss-rails' `tailwindcss:engines` task generates
157
+ # app/assets/builds/tailwind/lean_cms.css (a thin wrapper that @imports the
158
+ # gem's engine.css with absolute paths), but the host has to opt in by
159
+ # @import-ing that bundle file from its own application.css.
160
+ def wire_tailwind
161
+ tailwind_input = File.join(destination_root, "app/assets/tailwind/application.css")
162
+ unless File.exist?(tailwind_input)
163
+ say "Skipping Tailwind wire-up — no app/assets/tailwind/application.css found.", :yellow
164
+ say " If you use Tailwind, add this line to your Tailwind input file:", :yellow
165
+ say " @import \"../builds/tailwind/lean_cms.css\";", :cyan
166
+ return
167
+ end
168
+
169
+ contents = File.read(tailwind_input)
170
+ if contents.include?("builds/tailwind/lean_cms.css")
171
+ say "Tailwind input already imports lean_cms engine — skipping.", :cyan
172
+ return
173
+ end
174
+
175
+ say "Adding lean_cms engine @import to app/assets/tailwind/application.css", :green
176
+ append_to_file tailwind_input, <<~CSS
177
+
178
+ /* Lean CMS engine Tailwind sources (auto-generated by tailwindcss:engines). */
179
+ @import "../builds/tailwind/lean_cms.css";
180
+ CSS
181
+ end
182
+
183
+ # Add `import "trix"` + `import "@rails/actiontext"` to the host's
184
+ # application.js so the field-editor modal's Trix editor loads for
185
+ # rich_text fields. The gem's own importmap.rb already pins them;
186
+ # this step is just the import line in the host entry point.
187
+ def wire_actiontext_imports
188
+ app_js = File.join(destination_root, "app/javascript/application.js")
189
+ unless File.exist?(app_js)
190
+ say "Skipping Action Text JS wire-up — no app/javascript/application.js found.", :yellow
191
+ say " If you use importmap-rails, add these lines to your application.js:", :yellow
192
+ say " import \"trix\"", :cyan
193
+ say " import \"@rails/actiontext\"", :cyan
194
+ return
195
+ end
196
+
197
+ contents = File.read(app_js)
198
+ if contents.include?('import "trix"') || contents.include?("import 'trix'")
199
+ say "application.js already imports trix — skipping.", :cyan
200
+ return
201
+ end
202
+
203
+ say "Adding trix + @rails/actiontext imports to app/javascript/application.js", :green
204
+ append_to_file app_js, <<~JS
205
+
206
+ // Lean CMS uses Trix in the field-editor modal for rich_text fields.
207
+ import "trix"
208
+ import "@rails/actiontext"
209
+ JS
210
+ end
211
+
212
+ def run_migrations
213
+ rake "db:migrate"
214
+ end
215
+
216
+ def create_structure_file
217
+ target = File.join(destination_root, "config", "lean_cms_structure.yml")
218
+ return if File.exist?(target)
219
+ template "lean_cms_structure.yml", "config/lean_cms_structure.yml"
220
+ end
221
+
222
+ def print_instructions
223
+ say "\n#{"=" * 64}", :green
224
+ say "Lean CMS installed!", :green
225
+ say "=" * 64, :green
226
+ say ""
227
+ say "1. Configure your site", :yellow
228
+ say " Edit config/initializers/lean_cms.rb — site name, logo, colors,"
229
+ say " admin path, mailer_from."
230
+ say ""
231
+ say "2. Wire up your User model", :yellow
232
+ say " app/models/user.rb needs:"
233
+ say ""
234
+ say " class User < ApplicationRecord"
235
+ say " has_secure_password"
236
+ say ""
237
+ say " # Permission predicates that fall back to is_super_admin?:"
238
+ say " def can_edit_pages?; is_super_admin? || can_edit_pages; end"
239
+ say " def can_edit_blog?; is_super_admin? || can_edit_blog; end"
240
+ say " def can_manage_users?; is_super_admin? || can_manage_users; end"
241
+ say " def can_access_settings?; is_super_admin? || can_access_settings; end"
242
+ say " def has_any_cms_permission?"
243
+ say " can_edit_pages? || can_edit_blog? || can_manage_users? || can_access_settings?"
244
+ say " end"
245
+ say " def record_login!; update_column(:last_login_at, Time.current); end"
246
+ say " def active?; active; end"
247
+ say " def must_change_password?; must_change_password; end"
248
+ say ""
249
+ say " # Optional, but the admin nicely uses them when present:"
250
+ say " has_many :notifications, as: :recipient, dependent: :destroy,"
251
+ say " class_name: \"Noticed::Notification\""
252
+ say " def display_name; name.presence || email_address.split(\"@\").first; end"
253
+ say " def permissions_summary"
254
+ say " return \"Super Admin\" if is_super_admin?"
255
+ say " perms = []"
256
+ say " perms << \"Pages\" if can_edit_pages"
257
+ say " perms << \"Blog\" if can_edit_blog"
258
+ say " perms << \"Users\" if can_manage_users"
259
+ say " perms << \"Settings\" if can_access_settings"
260
+ say " perms.empty? ? \"No permissions\" : perms.join(\", \")"
261
+ say " end"
262
+ say " end"
263
+ say ""
264
+ say " Lean CMS uses LeanCms::Session and LeanCms::MagicLink directly — it does"
265
+ say " NOT require has_many :sessions or :magic_links on your User. That keeps"
266
+ say " it compatible with Rails 8's built-in auth and other auth gems."
267
+ say ""
268
+ say " Required columns on the users table: email_address (string, indexed unique),"
269
+ say " password_digest, name, active (boolean), must_change_password (boolean),"
270
+ say " last_login_at (datetime), and permission flags is_super_admin, can_edit_pages,"
271
+ say " can_edit_blog, can_manage_users, can_access_settings (all booleans)."
272
+ say ""
273
+ say "3. Include Lean CMS in ApplicationController", :yellow
274
+ say " class ApplicationController < ActionController::Base"
275
+ say " include LeanCms::Authentication"
276
+ say " end"
277
+ say ""
278
+ say " (Pundit is included automatically by the gem's admin controllers."
279
+ say " Add `include Pundit::Authorization` to your own ApplicationController"
280
+ say " only if you want `authorize` / `policy_scope` available in your"
281
+ say " non-CMS controllers too.)"
282
+ say ""
283
+ say "4. Include the helper in ApplicationHelper", :yellow
284
+ say " module ApplicationHelper"
285
+ say " include LeanCms::PageContentHelper"
286
+ say " end"
287
+ say ""
288
+ say "5. Add the admin bar to your public layout (optional but recommended)", :yellow
289
+ say " In app/views/layouts/application.html.erb:"
290
+ say ""
291
+ say " <body class=\"<%= 'pt-10' if current_user&.has_any_cms_permission? %>\">"
292
+ say " <%= cms_admin_bar %>"
293
+ say " <!-- your header / content -->"
294
+ say " </body>"
295
+ say ""
296
+ say " Gives signed-in editors a fixed strip with Inline Editing toggle,"
297
+ say " Help, Admin Dashboard, and Sign Out. Renders nothing for public visitors."
298
+ say ""
299
+ say "6. Seed your site structure", :yellow
300
+ say " Edit config/lean_cms_structure.yml, then:"
301
+ say " bin/rails lean_cms:load_structure"
302
+ say ""
303
+ say "7. Create your first admin", :yellow
304
+ say " bin/rails runner 'User.create!(email_address: \"admin@example.com\", password: \"change-me\", name: \"Admin\", active: true, is_super_admin: true)'"
305
+ say ""
306
+ say "8. Start the server and log in", :yellow
307
+ say " bin/dev (or rails server)"
308
+ say " Visit /lean-cms/login"
309
+ say ""
310
+ say "Optional: install demo pages — bin/rails generate lean_cms:demo", :cyan
311
+ say ""
312
+ say "Full docs: https://leancms.dev/docs/getting-started/", :cyan
313
+ say ""
314
+ end
315
+ end
316
+ end
317
+ end
@@ -0,0 +1,7 @@
1
+ class AddLeanCmsColumnsTo<%= @user_table.camelize %> < ActiveRecord::Migration[8.0]
2
+ def change
3
+ <% @missing_columns.each do |name, type, opts| -%>
4
+ add_column :<%= @user_table %>, :<%= name %>, :<%= type %><% if opts.any? %>, <%= opts.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") %><% end %>
5
+ <% end -%>
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ LeanCms.configure do |config|
2
+ config.site_name = "My Site"
3
+ config.site_logo_path = nil # e.g. "logo.png" from app/assets/images
4
+ config.primary_color = "#2563eb"
5
+ config.secondary_color = "#1e40af"
6
+ config.admin_path = "/lean-cms"
7
+ config.user_class = "<%= @user_class || 'User' %>"
8
+ config.posts_per_page = 10 # blog index pagination
9
+ config.portfolio_enabled = true # show the /portfolio routes + admin tab
10
+ config.mailer_from = "noreply@example.com"
11
+ end
@@ -0,0 +1,29 @@
1
+ pages:
2
+ home:
3
+ display_title: "Home"
4
+ page_order: 1
5
+ sections:
6
+ hero:
7
+ display_title: "Hero"
8
+ section_order: 1
9
+ fields:
10
+ heading:
11
+ label: "Headline"
12
+ type: text
13
+ default: "Welcome to My Site"
14
+ subheading:
15
+ label: "Subheadline"
16
+ type: text
17
+ default: "A great place to start"
18
+ body:
19
+ label: "Body Text"
20
+ type: rich_text
21
+ default: "<p>Tell visitors what makes your business special.</p>"
22
+ cta_text:
23
+ label: "CTA Button Text"
24
+ type: text
25
+ default: "Get Started"
26
+ cta_url:
27
+ label: "CTA Button URL"
28
+ type: url
29
+ default: "/contact"
@@ -0,0 +1,32 @@
1
+ module LeanCms
2
+ class << self
3
+ attr_accessor :site_name,
4
+ :site_logo_path,
5
+ :primary_color,
6
+ :secondary_color,
7
+ :admin_path,
8
+ :user_class,
9
+ :posts_per_page,
10
+ :portfolio_enabled,
11
+ :mailer_from,
12
+ :docs_url
13
+
14
+ def configure
15
+ yield self
16
+ end
17
+ end
18
+
19
+ # Defaults
20
+ self.site_name = "My Site"
21
+ self.site_logo_path = nil
22
+ self.primary_color = "#2563eb"
23
+ self.secondary_color = "#1e40af"
24
+ self.admin_path = "/lean-cms"
25
+ self.user_class = "User"
26
+ self.posts_per_page = 10
27
+ self.portfolio_enabled = true
28
+ self.mailer_from = "noreply@example.com"
29
+ # Where the "?" icon in the admin header sends editors. Override in your
30
+ # initializer to point at your internal handbook instead of the public docs.
31
+ self.docs_url = "https://leancms.dev/docs/"
32
+ end
@@ -0,0 +1,93 @@
1
+ module LeanCms
2
+ class Engine < ::Rails::Engine
3
+ # Note: isolate_namespace is intentionally omitted so that all lean_cms_* route
4
+ # helpers remain accessible in both the engine and host app without renaming views.
5
+
6
+ # Override Rails' default engine_name derivation ("lean_cms_engine") so that
7
+ # tailwindcss-rails' built-in engine discovery
8
+ # (Tailwindcss::Engines.bundle, run before every tailwindcss:build) finds
9
+ # our Tailwind sources at app/assets/tailwind/lean_cms/engine.css.
10
+ engine_name "lean_cms"
11
+
12
+ config.generators do |g|
13
+ g.test_framework :rspec
14
+ g.fixture_replacement :factory_bot
15
+ g.factory_bot dir: "spec/factories"
16
+ end
17
+
18
+ # Add gem's JS to Propshaft's load path so Stimulus controllers can be served.
19
+ # CSS (app/assets/lean_cms/) is discovered automatically via app/assets registration.
20
+ # Guarded — hosts without Propshaft (e.g. our test dummy app) don't expose
21
+ # config.assets at all.
22
+ initializer "lean_cms.assets" do |app|
23
+ app.config.assets.paths << root.join("app/javascript") if app.config.respond_to?(:assets)
24
+ end
25
+
26
+ # Register the gem's Stimulus controllers with the host app's importmap
27
+ initializer "lean_cms.importmap", before: "importmap" do |app|
28
+ if app.config.respond_to?(:importmap)
29
+ app.config.importmap.paths << root.join("config/importmap.rb")
30
+ end
31
+ end
32
+
33
+ # Make the gem's migrations available to the host app
34
+ initializer "lean_cms.migrations" do |app|
35
+ unless app.root.to_s == root.to_s
36
+ config.paths["db/migrate"].expanded.each do |path|
37
+ app.config.paths["db/migrate"] << path
38
+ end
39
+ end
40
+ end
41
+
42
+ # NOTE: We deliberately do NOT generate app/assets/builds/tailwind/lean_cms.css
43
+ # ourselves. That's tailwindcss-rails' job: Tailwindcss::Engines.bundle (run as
44
+ # the `tailwindcss:engines` task, which is a prereq of every `tailwindcss:build`
45
+ # and `tailwindcss:watch`) walks Rails::Engine.subclasses, finds ours by
46
+ # engine_name "lean_cms" (set above), and writes the bundle file containing
47
+ # @import "<gem>/app/assets/tailwind/lean_cms/engine.css". Hosts then pull
48
+ # this in from their own app/assets/tailwind/application.css — see the
49
+ # install generator's `wire_tailwind` step.
50
+
51
+ # Ensure host app always has a resolvable edit-controls stylesheet path.
52
+ # Place it under app/assets/stylesheets so stylesheet logical path resolution
53
+ # matches `stylesheet_link_tag "lean_cms/cms_edit_controls"`.
54
+ initializer "lean_cms.edit_controls_css" do |app|
55
+ next if app.root.to_s == root.to_s
56
+
57
+ host_css = app.root.join("app/assets/stylesheets/lean_cms/cms_edit_controls.css")
58
+ next if host_css.exist?
59
+
60
+ require "fileutils"
61
+ source_css = root.join("app/assets/lean_cms/cms_edit_controls.css")
62
+ next unless source_css.exist?
63
+
64
+ FileUtils.mkdir_p(host_css.dirname)
65
+ File.write(host_css, source_css.read)
66
+ end
67
+
68
+ # Action Text ships a CSS file via `bin/rails action_text:install`, but
69
+ # the gem's admin layout references it via `stylesheet_link_tag "actiontext"`
70
+ # before that step would normally run. Drop a default copy into the host's
71
+ # app/assets/stylesheets/ so a fresh install boots without a
72
+ # Propshaft::MissingAssetError. Hosts can edit / replace the file freely;
73
+ # we only write it once.
74
+ initializer "lean_cms.actiontext_css" do |app|
75
+ next if app.root.to_s == root.to_s
76
+
77
+ host_css = app.root.join("app/assets/stylesheets/actiontext.css")
78
+ next if host_css.exist?
79
+
80
+ require "fileutils"
81
+ source_css = root.join("app/assets/lean_cms/actiontext.css")
82
+ next unless source_css.exist?
83
+
84
+ FileUtils.mkdir_p(host_css.dirname)
85
+ File.write(host_css, source_css.read)
86
+ end
87
+
88
+ # Load rake tasks
89
+ rake_tasks do
90
+ load LeanCms::Engine.root.join("lib/tasks/lean_cms.rake")
91
+ end
92
+ end
93
+ end