macron-cms 1.0.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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +265 -0
  4. data/Rakefile +4 -0
  5. data/app/assets/stylesheets/macron/cms/application.css +2 -0
  6. data/app/assets/tailwind/macron/cms/application.css +4 -0
  7. data/app/components/macron/cms/widget_component.rb +29 -0
  8. data/app/controllers/macron/cms/application_controller.rb +36 -0
  9. data/app/controllers/macron/cms/article_contents_controller.rb +103 -0
  10. data/app/controllers/macron/cms/articles_controller.rb +56 -0
  11. data/app/controllers/macron/cms/content_types_controller.rb +20 -0
  12. data/app/controllers/macron/cms/dashboard_controller.rb +10 -0
  13. data/app/helpers/macron/cms/application_helper.rb +40 -0
  14. data/app/helpers/macron/cms/content_helper.rb +31 -0
  15. data/app/helpers/macron/cms/fields_helper.rb +30 -0
  16. data/app/helpers/macron/cms/seo_helper.rb +37 -0
  17. data/app/jobs/macron/cms/application_job.rb +6 -0
  18. data/app/mailers/macron/cms/application_mailer.rb +8 -0
  19. data/app/models/macron/cms/application_record.rb +7 -0
  20. data/app/models/macron/cms/article.rb +42 -0
  21. data/app/models/macron/cms/article_content.rb +31 -0
  22. data/app/models/macron/cms/content.rb +34 -0
  23. data/app/views/layouts/macron/cms/application.html.erb +129 -0
  24. data/app/views/macron/cms/articles/_form.html.erb +65 -0
  25. data/app/views/macron/cms/articles/_table.html.erb +54 -0
  26. data/app/views/macron/cms/articles/edit.html.erb +15 -0
  27. data/app/views/macron/cms/articles/index.html.erb +28 -0
  28. data/app/views/macron/cms/articles/new.html.erb +14 -0
  29. data/app/views/macron/cms/articles/show.html.erb +366 -0
  30. data/app/views/macron/cms/content_types/index.html.erb +87 -0
  31. data/app/views/macron/cms/content_types/show.html.erb +148 -0
  32. data/app/views/macron/cms/dashboard/index.html.erb +81 -0
  33. data/app/views/macron/cms/fields/_string.html.erb +14 -0
  34. data/app/views/macron/cms/fields/_text.html.erb +13 -0
  35. data/app/views/macron/cms/shared/_breadcrumbs.html.erb +14 -0
  36. data/config/routes.rb +14 -0
  37. data/db/migrate/20261028000001_create_macron_cms_articles.rb +9 -0
  38. data/db/migrate/20261028000002_add_name_and_slug_to_macron_cms_articles.rb +8 -0
  39. data/db/migrate/20261028000003_add_seo_metadata_to_macron_cms_articles.rb +6 -0
  40. data/db/migrate/20261028000004_create_macron_cms_contents.rb +13 -0
  41. data/db/migrate/20261028000005_create_macron_cms_article_contents.rb +13 -0
  42. data/db/migrate/20261028000006_move_payload_to_macron_cms_article_contents.rb +6 -0
  43. data/db/seeds.rb +83 -0
  44. data/lib/generators/macron/cms/content_type/content_type_generator.rb +88 -0
  45. data/lib/macron/cms/configuration.rb +25 -0
  46. data/lib/macron/cms/engine.rb +20 -0
  47. data/lib/macron/cms/version.rb +5 -0
  48. data/lib/macron/cms.rb +26 -0
  49. data/lib/tasks/macron/cms_tasks.rake +75 -0
  50. metadata +164 -0
@@ -0,0 +1,81 @@
1
+ <% content_for :title, "Dashboard - Macron CMS" %>
2
+ <% cms_breadcrumb "Dashboard" %>
3
+
4
+ <div class="space-y-8">
5
+ <!-- Page Header -->
6
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 border-b border-zinc-200 pb-6">
7
+ <div>
8
+ <h1 class="text-2xl font-bold tracking-tight text-zinc-900 sm:text-3xl">CMS Dashboard</h1>
9
+ <p class="mt-1 text-sm text-zinc-500">Overview of your published content, system health, and quick actions.</p>
10
+ </div>
11
+ <div class="flex items-center gap-3">
12
+ <%= link_to new_article_path, class: "inline-flex items-center gap-2 rounded-lg bg-zinc-900 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-zinc-800 transition-colors" do %>
13
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
14
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
15
+ </svg>
16
+ New Article
17
+ <% end %>
18
+ </div>
19
+ </div>
20
+
21
+ <!-- Metric Stat Cards -->
22
+ <div class="grid grid-cols-1 gap-5 sm:grid-cols-3">
23
+ <!-- Stat 1: Total Articles -->
24
+ <div class="overflow-hidden rounded-xl bg-white p-6 border border-zinc-200 shadow-xs">
25
+ <div class="flex items-center justify-between">
26
+ <dt class="truncate text-sm font-medium text-zinc-500">Total Articles</dt>
27
+ <span class="rounded-md bg-zinc-100 p-2 text-zinc-700">
28
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
29
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
30
+ </svg>
31
+ </span>
32
+ </div>
33
+ <dd class="mt-3 text-3xl font-bold tracking-tight text-zinc-900"><%= @articles_count %></dd>
34
+ <p class="mt-1 text-xs text-zinc-500">Managed via Macron CMS</p>
35
+ </div>
36
+
37
+ <!-- Stat 2: Engine Version -->
38
+ <div class="overflow-hidden rounded-xl bg-white p-6 border border-zinc-200 shadow-xs">
39
+ <div class="flex items-center justify-between">
40
+ <dt class="truncate text-sm font-medium text-zinc-500">Engine Version</dt>
41
+ <span class="rounded-md bg-zinc-100 p-2 text-zinc-700">
42
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
43
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 0 1 0 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 0 1 0-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281Z" />
44
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
45
+ </svg>
46
+ </span>
47
+ </div>
48
+ <dd class="mt-3 text-3xl font-bold tracking-tight text-zinc-900 font-mono">v<%= Macron::Cms::VERSION %></dd>
49
+ <p class="mt-1 text-xs text-zinc-500">Standalone Tailwind Enabled</p>
50
+ </div>
51
+
52
+ <!-- Stat 3: Access Guard -->
53
+ <div class="overflow-hidden rounded-xl bg-white p-6 border border-zinc-200 shadow-xs">
54
+ <div class="flex items-center justify-between">
55
+ <dt class="truncate text-sm font-medium text-zinc-500">Access Guard</dt>
56
+ <span class="rounded-md bg-emerald-50 p-2 text-emerald-700">
57
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
58
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" />
59
+ </svg>
60
+ </span>
61
+ </div>
62
+ <dd class="mt-3 text-3xl font-bold tracking-tight text-zinc-900">Protected</dd>
63
+ <p class="mt-1 text-xs text-zinc-500">Devise <%= Macron::Cms.configuration.user_class %> (<%= Macron::Cms.configuration.admin_flag %>: true)</p>
64
+ </div>
65
+ </div>
66
+
67
+ <!-- Recent Articles Section -->
68
+ <div class="space-y-4">
69
+ <div class="flex items-center justify-between">
70
+ <div>
71
+ <h2 class="text-lg font-semibold text-zinc-900">Recent Articles</h2>
72
+ <p class="text-xs text-zinc-500">Latest content items updated or created in the engine.</p>
73
+ </div>
74
+ <% if @articles_count > 0 %>
75
+ <%= link_to "View all articles &rarr;".html_safe, articles_path, class: "text-sm font-medium text-zinc-700 hover:text-zinc-900 transition-colors" %>
76
+ <% end %>
77
+ </div>
78
+
79
+ <%= render "macron/cms/articles/table", articles: @recent_articles %>
80
+ </div>
81
+ </div>
@@ -0,0 +1,14 @@
1
+ <div class="space-y-1.5">
2
+ <label for="<%= input_id %>" class="block text-xs font-mono font-semibold text-zinc-700">
3
+ <%= field_name %>
4
+ <span class="ml-1 text-[10px] font-normal text-zinc-400">(string)</span>
5
+ </label>
6
+ <input
7
+ type="text"
8
+ name="<%= input_name %>"
9
+ id="<%= input_id %>"
10
+ value="<%= value %>"
11
+ class="block w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 shadow-2xs placeholder:text-zinc-400 focus:border-zinc-900 focus:outline-none focus:ring-1 focus:ring-zinc-900"
12
+ placeholder="Enter <%= field_name.to_s.humanize.downcase %>..."
13
+ />
14
+ </div>
@@ -0,0 +1,13 @@
1
+ <div class="space-y-1.5">
2
+ <label for="<%= input_id %>" class="block text-xs font-mono font-semibold text-zinc-700">
3
+ <%= field_name %>
4
+ <span class="ml-1 text-[10px] font-normal text-zinc-400">(text)</span>
5
+ </label>
6
+ <textarea
7
+ name="<%= input_name %>"
8
+ id="<%= input_id %>"
9
+ rows="4"
10
+ class="block w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 shadow-2xs placeholder:text-zinc-400 focus:border-zinc-900 focus:outline-none focus:ring-1 focus:ring-zinc-900"
11
+ placeholder="Enter <%= field_name.to_s.humanize.downcase %>..."
12
+ ><%= value %></textarea>
13
+ </div>
@@ -0,0 +1,14 @@
1
+ <% if cms_breadcrumbs.any? %>
2
+ <% cms_breadcrumbs.each_with_index do |crumb, index| %>
3
+ <% if index > 0 %>
4
+ <span class="text-zinc-400 select-none">/</span>
5
+ <% end %>
6
+ <% if index == cms_breadcrumbs.size - 1 || crumb[:path].blank? %>
7
+ <span class="font-medium text-zinc-900 truncate max-w-xs sm:max-w-md"><%= crumb[:label] %></span>
8
+ <% else %>
9
+ <%= link_to crumb[:label], crumb[:path], class: "hover:text-zinc-900 transition-colors" %>
10
+ <% end %>
11
+ <% end %>
12
+ <% else %>
13
+ <span class="font-medium text-zinc-900"><%= content_for(:header_title) || "Overview" %></span>
14
+ <% end %>
data/config/routes.rb ADDED
@@ -0,0 +1,14 @@
1
+ Macron::Cms::Engine.routes.draw do
2
+ root to: "dashboard#index"
3
+ resources :articles do
4
+ resources :article_contents, only: %i[create update destroy] do
5
+ member do
6
+ patch :move
7
+ end
8
+ collection do
9
+ patch :reorder
10
+ end
11
+ end
12
+ end
13
+ resources :content_types, only: %i[index show]
14
+ end
@@ -0,0 +1,9 @@
1
+ class CreateMacronCmsArticles < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :macron_cms_articles do |t|
4
+ t.string :title, null: false
5
+
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ class AddNameAndSlugToMacronCmsArticles < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :macron_cms_articles, :name, :string, null: false
4
+ add_column :macron_cms_articles, :slug, :string, null: false
5
+ add_index :macron_cms_articles, :name, unique: true
6
+ add_index :macron_cms_articles, :slug, unique: true
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ class AddSeoMetadataToMacronCmsArticles < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :macron_cms_articles, :meta_title, :string
4
+ add_column :macron_cms_articles, :meta_description, :text
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ class CreateMacronCmsContents < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :macron_cms_contents do |t|
4
+ t.string :name, null: false
5
+ t.jsonb :fields, default: {}, null: false
6
+ t.jsonb :payload, default: {}, null: false
7
+
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :macron_cms_contents, :name, unique: true
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ class CreateMacronCmsArticleContents < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :macron_cms_article_contents do |t|
4
+ t.references :article, null: false, foreign_key: { to_table: :macron_cms_articles }
5
+ t.references :content, null: false, foreign_key: { to_table: :macron_cms_contents }
6
+ t.integer :sort_order, default: 0, null: false
7
+
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :macron_cms_article_contents, [ :article_id, :sort_order ]
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ class MovePayloadToMacronCmsArticleContents < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :macron_cms_article_contents, :payload, :jsonb, default: {}, null: false
4
+ remove_column :macron_cms_contents, :payload, :jsonb, default: {}, null: false
5
+ end
6
+ end
data/db/seeds.rb ADDED
@@ -0,0 +1,83 @@
1
+ articles = [
2
+ {
3
+ title: "Getting Started with MacronCMS",
4
+ meta_title: "Getting Started with MacronCMS | Rails 8 Content Engine",
5
+ meta_description: "Discover how to install, configure, and build interactive content architectures with MacronCMS on Rails 8."
6
+ },
7
+ {
8
+ title: "Designing Content Structures with Rails 8",
9
+ meta_title: "Designing Content Structures with Rails 8 | MacronCMS Guide",
10
+ meta_description: "A comprehensive guide to modeling content hierarchies, flexible attributes, and schema migrations in Rails 8."
11
+ },
12
+ {
13
+ title: "The wp_loop Architecture: Clean Content Iteration",
14
+ meta_title: "Understanding The wp_loop Architecture in Rails | MacronCMS",
15
+ meta_description: "Explore the wp_loop-inspired data pattern for declarative, clean, and decoupled content iteration in Ruby on Rails."
16
+ },
17
+ {
18
+ title: "Hydrating React and Three.js in Modern Server-Rendered Views",
19
+ meta_title: "Hydrating React and Three.js in Rails Views | MacronCMS",
20
+ meta_description: "Seamlessly integrate interactive React components and Three.js 3D scenes into server-rendered Rails templates."
21
+ },
22
+ {
23
+ title: "Tailwind CSS Layouts for Dynamic Content",
24
+ meta_title: "Tailwind CSS Layouts for Dynamic Content Publishing | MacronCMS",
25
+ meta_description: "Master modern utility-first typography, fluid layouts, and responsive components for publishing with Tailwind CSS."
26
+ },
27
+ {
28
+ title: "High-Performance Publishing with Hotwire and Turbo",
29
+ meta_title: "High-Performance Publishing with Hotwire & Turbo | MacronCMS",
30
+ meta_description: "Boost site speed and user experience by leveraging Turbo Drive, Turbo Frames, and Turbo Streams in your CMS."
31
+ },
32
+ {
33
+ title: "Migrating Legacy Content into MacronCMS with AI Agents",
34
+ meta_title: "Migrating Legacy Content with AI Agents | MacronCMS",
35
+ meta_description: "How to use autonomous AI agents to transform, clean, and import unstructured legacy datasets into MacronCMS."
36
+ },
37
+ {
38
+ title: "Building Accessible Digital Experiences with Rails",
39
+ meta_title: "Building Accessible Web Experiences with Rails | MacronCMS",
40
+ meta_description: "Best practices and strategies for building WCAG-compliant, accessible content layouts and UI controls in Rails."
41
+ },
42
+ {
43
+ title: "Optimizing Content Queries and Asset Pipelines",
44
+ meta_title: "Optimizing Content Queries and Asset Pipelines | MacronCMS",
45
+ meta_description: "Techniques for eliminating N+1 queries, optimizing cache keys, and streamlining modern asset delivery pipelines."
46
+ },
47
+ {
48
+ title: "The Future of Developer-First Content Management Systems",
49
+ meta_title: "The Future of Developer-First CMS Platforms | MacronCMS",
50
+ meta_description: "An editorial on modularity, headless workflows, AI integration, and the evolution of developer-first CMS platforms."
51
+ }
52
+ ]
53
+
54
+ articles.each do |data|
55
+ article = Macron::Cms::Article.find_or_initialize_by(name: data[:title].parameterize)
56
+ article.title = data[:title]
57
+ article.meta_title = data[:meta_title]
58
+ article.meta_description = data[:meta_description]
59
+ article.save!
60
+ end
61
+
62
+ puts "Seeded #{Macron::Cms::Article.count} Macron::Cms articles."
63
+
64
+ user_class_name = Macron::Cms.configuration.user_class
65
+ user_class = user_class_name.safe_constantize
66
+
67
+ if user_class
68
+ admin_flag = Macron::Cms.configuration.admin_flag
69
+
70
+ admin_user = user_class.find_or_initialize_by(email: "admin@example.com")
71
+ admin_user.password = "password" if admin_user.respond_to?(:password=)
72
+ admin_user.password_confirmation = "password" if admin_user.respond_to?(:password_confirmation=)
73
+ admin_user.public_send(:"#{admin_flag}=", true) if admin_user.respond_to?(:"#{admin_flag}=")
74
+ admin_user.save!
75
+
76
+ regular_user = user_class.find_or_initialize_by(email: "user@example.com")
77
+ regular_user.password = "password" if regular_user.respond_to?(:password=)
78
+ regular_user.password_confirmation = "password" if regular_user.respond_to?(:password_confirmation=)
79
+ regular_user.public_send(:"#{admin_flag}=", false) if regular_user.respond_to?(:"#{admin_flag}=")
80
+ regular_user.save!
81
+
82
+ puts "Seeded test users (admin: admin@example.com, regular: user@example.com)."
83
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module Macron
6
+ module Cms
7
+ module Generators
8
+ class ContentTypeGenerator < ::Rails::Generators::NamedBase
9
+ desc "Generates a Macron CMS content type YAML definition file and a matching ViewComponent widget in app/components/widgets/."
10
+
11
+ argument :fields, type: :array, default: [], banner: "field:kind field:kind"
12
+
13
+ def create_content_type_file
14
+ parsed = parse_and_validate_fields
15
+ target_path = File.join(base_directory.to_s, "#{file_name}.yml")
16
+ create_file target_path, build_yaml(parsed)
17
+ end
18
+
19
+ def create_widget_component_files
20
+ parsed = parse_and_validate_fields
21
+ component_path = File.join("app/components/widgets", "#{file_name}_component.rb")
22
+ template_path = File.join("app/components/widgets", "#{file_name}_component.html.erb")
23
+
24
+ create_file component_path, build_widget_ruby
25
+ create_file template_path, build_widget_template(parsed)
26
+ end
27
+
28
+ private
29
+
30
+ def base_directory
31
+ Macron::Cms.configuration.content_types_path || "config/content_types"
32
+ end
33
+
34
+ def parse_and_validate_fields
35
+ @parsed_fields ||= fields.map do |field|
36
+ parts = field.split(":", 2)
37
+ if parts.length != 2 || parts[0].blank? || parts[1].blank?
38
+ raise Thor::Error, "Invalid field '#{field}'. Fields must be in the format 'name:kind' (e.g. title:string)."
39
+ end
40
+
41
+ { "name" => parts[0], "kind" => parts[1] }
42
+ end
43
+ end
44
+
45
+ def build_yaml(parsed_fields)
46
+ if parsed_fields.empty?
47
+ "fields: []\n"
48
+ else
49
+ lines = [ "fields:" ]
50
+ parsed_fields.each do |f|
51
+ lines << " - name: '#{f['name']}'"
52
+ lines << " kind: '#{f['kind']}'"
53
+ end
54
+ "#{lines.join("\n")}\n"
55
+ end
56
+ end
57
+
58
+ def build_widget_ruby
59
+ <<~RUBY
60
+ module Widgets
61
+ class #{file_name.camelize}Component < Macron::Cms::WidgetComponent
62
+ end
63
+ end
64
+ RUBY
65
+ end
66
+
67
+ def build_widget_template(parsed_fields)
68
+ if parsed_fields.empty?
69
+ <<~ERB
70
+ <div class="my-4">
71
+ <!-- #{file_name.camelize} Widget -->
72
+ </div>
73
+ ERB
74
+ else
75
+ field_tags = parsed_fields.map do |f|
76
+ " <p><%= #{f['name']} %></p>"
77
+ end
78
+ <<~ERB
79
+ <div class="my-4">
80
+ #{field_tags.join("\n")}
81
+ </div>
82
+ ERB
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,25 @@
1
+ module Macron
2
+ module Cms
3
+ class Configuration
4
+ attr_accessor :user_class, :admin_flag, :content_types_path
5
+
6
+ def initialize
7
+ @user_class = "User"
8
+ @admin_flag = :admin
9
+ @content_types_path = nil
10
+ end
11
+
12
+ def devise_scope
13
+ user_class.to_s.underscore.to_sym
14
+ end
15
+
16
+ def authenticate_method
17
+ :"authenticate_#{devise_scope}!"
18
+ end
19
+
20
+ def current_user_method
21
+ :"current_#{devise_scope}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ module Macron
2
+ module Cms
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace Macron::Cms
5
+
6
+ initializer :append_migrations do |app|
7
+ config.paths["db/migrate"].expanded.each do |expanded_path|
8
+ app.config.paths["db/migrate"] << expanded_path
9
+ end
10
+ end
11
+
12
+ initializer "macron_cms.helpers" do
13
+ ActiveSupport.on_load(:action_view) do
14
+ include Macron::Cms::SeoHelper
15
+ include Macron::Cms::ContentHelper
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ module Macron
2
+ module Cms
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
data/lib/macron/cms.rb ADDED
@@ -0,0 +1,26 @@
1
+ require "friendly_id"
2
+ require "devise"
3
+ require "view_component"
4
+ require "macron/cms/version"
5
+ require "macron/cms/configuration"
6
+ require "macron/cms/engine"
7
+
8
+ module Macron
9
+ module Cms
10
+ class MissingWidgetError < StandardError; end
11
+
12
+ class << self
13
+ def configuration
14
+ @configuration ||= Configuration.new
15
+ end
16
+
17
+ def configure
18
+ yield(configuration)
19
+ end
20
+
21
+ def reset_configuration!
22
+ @configuration = Configuration.new
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,75 @@
1
+ namespace :macron do
2
+ namespace :cms do
3
+ desc "Import content types from YAML files into Macron::Cms::Content"
4
+ task import_content_types: :environment do
5
+ require "yaml"
6
+
7
+ base_path = Macron::Cms.configuration.content_types_path ||
8
+ (defined?(Rails.root) && Rails.root ? Rails.root.join("config", "content_types") : File.expand_path("config/content_types"))
9
+ directory = Pathname.new(base_path)
10
+
11
+ unless directory.exist?
12
+ puts "Content types directory '#{directory}' does not exist. Skipping import."
13
+ next
14
+ end
15
+
16
+ files = Dir.glob(directory.join("*.{yml,yaml}")).sort
17
+
18
+ if files.empty?
19
+ puts "No YAML content types found in #{directory}."
20
+ next
21
+ end
22
+
23
+ files.each do |file|
24
+ content_type_name = File.basename(file, ".*")
25
+ data = YAML.safe_load_file(file, permitted_classes: [ Symbol ], aliases: true) || {}
26
+ fields = data["fields"] || data[:fields] || {}
27
+
28
+ content = Macron::Cms::Content.find_or_initialize_by(name: content_type_name)
29
+ content.fields = fields
30
+ content.save!
31
+
32
+ puts "Imported content type: #{content_type_name}"
33
+ end
34
+ end
35
+
36
+ namespace :content do
37
+ desc "Alias for macron:cms:import_content_types"
38
+ task import: :"macron:cms:import_content_types"
39
+ end
40
+
41
+ namespace :tailwind do
42
+ def tailwind_executable
43
+ require "tailwindcss/ruby"
44
+ Tailwindcss::Ruby.executable
45
+ rescue LoadError
46
+ "tailwindcss"
47
+ end
48
+
49
+ desc "Build Macron CMS Tailwind CSS"
50
+ task :build do
51
+ engine_root = File.expand_path("../../..", __dir__)
52
+ input = File.join(engine_root, "app/assets/tailwind/macron/cms/application.css")
53
+ output = File.join(engine_root, "app/assets/stylesheets/macron/cms/application.css")
54
+
55
+ exe = tailwind_executable
56
+ cmd = [ exe, "-i", input, "-o", output, "--minify" ]
57
+ puts "Building Macron CMS Tailwind CSS..."
58
+ success = system(*cmd, chdir: engine_root)
59
+ abort("Tailwind build failed") unless success
60
+ end
61
+
62
+ desc "Watch Macron CMS Tailwind CSS for changes"
63
+ task :watch do
64
+ engine_root = File.expand_path("../../..", __dir__)
65
+ input = File.join(engine_root, "app/assets/tailwind/macron/cms/application.css")
66
+ output = File.join(engine_root, "app/assets/stylesheets/macron/cms/application.css")
67
+
68
+ exe = tailwind_executable
69
+ cmd = [ exe, "-i", input, "-o", output, "-w" ]
70
+ puts "Watching Macron CMS Tailwind CSS..."
71
+ system(*cmd, chdir: engine_root)
72
+ end
73
+ end
74
+ end
75
+ end