panda-cms 0.8.2 → 0.10.2

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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +75 -5
  3. data/app/components/panda/cms/code_component.rb +154 -39
  4. data/app/components/panda/cms/grid_component.rb +26 -6
  5. data/app/components/panda/cms/menu_component.rb +72 -34
  6. data/app/components/panda/cms/page_menu_component.rb +102 -13
  7. data/app/components/panda/cms/rich_text_component.rb +229 -139
  8. data/app/components/panda/cms/text_component.rb +107 -42
  9. data/app/controllers/panda/cms/admin/base_controller.rb +19 -3
  10. data/app/controllers/panda/cms/admin/dashboard_controller.rb +3 -3
  11. data/app/controllers/panda/cms/admin/files_controller.rb +7 -0
  12. data/app/controllers/panda/cms/admin/menus_controller.rb +47 -3
  13. data/app/controllers/panda/cms/admin/pages_controller.rb +11 -2
  14. data/app/controllers/panda/cms/admin/posts_controller.rb +3 -1
  15. data/app/controllers/panda/cms/form_submissions_controller.rb +134 -11
  16. data/app/controllers/panda/cms/pages_controller.rb +7 -2
  17. data/app/controllers/panda/cms/posts_controller.rb +16 -0
  18. data/app/helpers/panda/cms/application_helper.rb +17 -4
  19. data/app/helpers/panda/cms/asset_helper.rb +14 -61
  20. data/app/helpers/panda/cms/forms_helper.rb +60 -0
  21. data/app/helpers/panda/cms/seo_helper.rb +85 -0
  22. data/app/javascript/panda/cms/{application_panda_cms.js → application.js} +5 -1
  23. data/app/javascript/panda/cms/controllers/code_editor_controller.js +95 -0
  24. data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +31 -4
  25. data/app/javascript/panda/cms/controllers/file_gallery_controller.js +128 -0
  26. data/app/javascript/panda/cms/controllers/file_upload_controller.js +165 -0
  27. data/app/javascript/panda/cms/controllers/index.js +54 -13
  28. data/app/javascript/panda/cms/controllers/inline_code_editor_controller.js +96 -0
  29. data/app/javascript/panda/cms/controllers/menu_form_controller.js +53 -0
  30. data/app/javascript/panda/cms/controllers/nested_form_controller.js +35 -0
  31. data/app/javascript/panda/cms/controllers/page_form_controller.js +454 -0
  32. data/app/javascript/panda/cms/controllers/tree_controller.js +214 -0
  33. data/app/javascript/panda/cms/stimulus-loading.js +6 -7
  34. data/app/models/panda/cms/block_content.rb +9 -0
  35. data/app/models/panda/cms/menu.rb +12 -0
  36. data/app/models/panda/cms/page.rb +147 -0
  37. data/app/models/panda/cms/post.rb +98 -0
  38. data/app/views/layouts/homepage.html.erb +1 -4
  39. data/app/views/layouts/page.html.erb +1 -4
  40. data/app/views/panda/cms/admin/dashboard/show.html.erb +5 -5
  41. data/app/views/panda/cms/admin/files/_file_details.html.erb +45 -0
  42. data/app/views/panda/cms/admin/files/index.html.erb +11 -118
  43. data/app/views/panda/cms/admin/forms/index.html.erb +2 -2
  44. data/app/views/panda/cms/admin/forms/new.html.erb +1 -2
  45. data/app/views/panda/cms/admin/forms/show.html.erb +15 -30
  46. data/app/views/panda/cms/admin/menus/_menu_item_fields.html.erb +11 -0
  47. data/app/views/panda/cms/admin/menus/edit.html.erb +62 -0
  48. data/app/views/panda/cms/admin/menus/index.html.erb +3 -2
  49. data/app/views/panda/cms/admin/menus/new.html.erb +38 -0
  50. data/app/views/panda/cms/admin/pages/edit.html.erb +147 -22
  51. data/app/views/panda/cms/admin/pages/index.html.erb +49 -11
  52. data/app/views/panda/cms/admin/pages/new.html.erb +3 -11
  53. data/app/views/panda/cms/admin/posts/_form.html.erb +44 -15
  54. data/app/views/panda/cms/admin/posts/edit.html.erb +2 -2
  55. data/app/views/panda/cms/admin/posts/index.html.erb +6 -6
  56. data/app/views/panda/cms/admin/posts/new.html.erb +1 -1
  57. data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +1 -1
  58. data/app/views/panda/cms/admin/settings/index.html.erb +3 -3
  59. data/app/views/shared/_header.html.erb +1 -4
  60. data/config/brakeman.ignore +38 -0
  61. data/config/importmap.rb +10 -10
  62. data/config/initializers/panda/cms/healthcheck_log_silencer.rb.disabled +31 -0
  63. data/config/initializers/panda/cms.rb +52 -10
  64. data/config/locales/en.yml +41 -0
  65. data/config/routes.rb +5 -3
  66. data/db/migrate/20240305000000_convert_html_content_to_editor_js.rb +2 -2
  67. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +6 -1
  68. data/db/migrate/20250809231125_migrate_users_to_panda_core.rb +23 -21
  69. data/db/migrate/20251104150640_add_cached_last_updated_at_to_panda_cms_pages.rb +22 -0
  70. data/db/migrate/20251104172242_add_page_type_to_panda_cms_pages.rb +6 -0
  71. data/db/migrate/20251104172638_set_page_types_for_existing_pages.rb +27 -0
  72. data/db/migrate/20251105000001_add_pending_review_status_to_pages_and_posts.panda_cms.rb +21 -0
  73. data/db/migrate/20251109131150_add_seo_fields_to_pages.rb +32 -0
  74. data/db/migrate/20251109131205_add_seo_fields_to_posts.rb +27 -0
  75. data/db/migrate/20251110114258_add_spam_tracking_to_form_submissions.rb +7 -0
  76. data/db/migrate/20251110122812_add_performance_indexes_to_pages_and_redirects.rb +13 -0
  77. data/lib/generators/panda/cms/install_generator.rb +2 -5
  78. data/lib/panda/cms/asset_loader.rb +46 -76
  79. data/lib/panda/cms/bulk_editor.rb +288 -12
  80. data/lib/panda/cms/debug.rb +29 -0
  81. data/lib/panda/cms/engine/asset_config.rb +49 -0
  82. data/lib/panda/cms/engine/autoload_config.rb +19 -0
  83. data/lib/panda/cms/engine/backtrace_config.rb +42 -0
  84. data/lib/panda/cms/engine/core_config.rb +106 -0
  85. data/lib/panda/cms/engine/helper_config.rb +20 -0
  86. data/lib/panda/cms/engine/route_config.rb +34 -0
  87. data/lib/panda/cms/engine/view_component_config.rb +31 -0
  88. data/lib/panda/cms/engine.rb +44 -162
  89. data/lib/panda/cms/features.rb +52 -0
  90. data/lib/panda/cms.rb +10 -0
  91. data/lib/panda-cms/version.rb +1 -1
  92. data/lib/panda-cms.rb +20 -7
  93. data/lib/tasks/panda_cms_tasks.rake +16 -0
  94. metadata +41 -50
  95. data/app/components/panda/cms/admin/container_component.html.erb +0 -13
  96. data/app/components/panda/cms/admin/flash_message_component.html.erb +0 -31
  97. data/app/components/panda/cms/admin/panel_component.html.erb +0 -7
  98. data/app/components/panda/cms/admin/slideover_component.html.erb +0 -9
  99. data/app/components/panda/cms/admin/slideover_component.rb +0 -15
  100. data/app/components/panda/cms/admin/statistics_component.html.erb +0 -4
  101. data/app/components/panda/cms/admin/statistics_component.rb +0 -16
  102. data/app/components/panda/cms/admin/tab_bar_component.html.erb +0 -35
  103. data/app/components/panda/cms/admin/tab_bar_component.rb +0 -15
  104. data/app/components/panda/cms/admin/table_component.html.erb +0 -29
  105. data/app/components/panda/cms/admin/user_activity_component.html.erb +0 -7
  106. data/app/components/panda/cms/admin/user_activity_component.rb +0 -20
  107. data/app/components/panda/cms/admin/user_display_component.html.erb +0 -17
  108. data/app/components/panda/cms/admin/user_display_component.rb +0 -21
  109. data/app/components/panda/cms/grid_component.html.erb +0 -6
  110. data/app/components/panda/cms/menu_component.html.erb +0 -6
  111. data/app/components/panda/cms/page_menu_component.html.erb +0 -21
  112. data/app/components/panda/cms/rich_text_component.html.erb +0 -90
  113. data/app/javascript/panda_cms/stimulus-loading.js +0 -39
  114. data/app/views/layouts/panda/cms/application.html.erb +0 -42
  115. data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +0 -28
  116. data/app/views/panda/cms/admin/shared/_flash.html.erb +0 -5
  117. data/app/views/panda/cms/admin/shared/_sidebar.html.erb +0 -41
  118. data/app/views/panda/cms/shared/_footer.html.erb +0 -2
  119. data/app/views/panda/cms/shared/_header.html.erb +0 -25
  120. data/app/views/panda/cms/shared/_importmap.html.erb +0 -34
  121. data/config/initializers/inflections.rb +0 -5
  122. data/config/initializers/panda/cms/healthcheck_log_silencer.rb +0 -13
  123. data/lib/tasks/assets.rake +0 -587
data/config/importmap.rb CHANGED
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- pin "application_panda_cms", to: "panda_cms/application_panda_cms.js", preload: true
3
+ # Base dependencies are now in panda-core (Stimulus, Turbo, Font Awesome, etc.)
4
+ # This file only contains CMS-specific pins
5
+ # NOTE: Paths must be absolute (starting with /) because Rack::Static serves
6
+ # from /panda/cms/, not from asset pipeline /assets/
4
7
 
5
- pin "@hotwired/turbo", to: "@hotwired--turbo.js", preload: true # @8.0.12
6
- pin "@rails/actioncable/src", to: "@rails--actioncable--src.js", preload: true # @7.2.101
7
- pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2
8
- pin "@hotwired/stimulus-loading", to: "panda_cms/stimulus-loading.js", preload: true
9
- pin "tailwindcss-stimulus-components" # @6.1.2
10
- pin "@editorjs/editorjs", to: "panda/cms/editor/editorjs.js" # @2.30.6
8
+ pin "panda/cms/application", to: "/panda/cms/application.js", preload: true
9
+ pin "@hotwired/stimulus-loading", to: "/panda/cms/stimulus-loading.js", preload: true
10
+ pin "@editorjs/editorjs", to: "/panda/cms/editor/editorjs.js" # @2.30.6
11
11
 
12
12
  # Pin the controllers directory
13
- pin "controllers", to: "panda/cms/controllers/index.js"
14
- pin_all_from Panda::CMS::Engine.root.join("app/javascript/panda/cms/controllers"), under: "controllers"
15
- pin_all_from Panda::CMS::Engine.root.join("app/javascript/panda/cms/editor"), under: "editor"
13
+ pin "panda/cms/controllers/index", to: "/panda/cms/controllers/index.js"
14
+ pin_all_from Panda::CMS::Engine.root.join("app/javascript/panda/cms/controllers"), under: "controllers", to: "/panda/cms/controllers"
15
+ pin_all_from Panda::CMS::Engine.root.join("app/javascript/panda/cms/editor"), under: "editor", to: "/panda/cms/editor"
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "silencer/rails/logger"
4
+
5
+ # Don't log requests to the healthcheck endpoint
6
+ Rails.application.configure do
7
+ # Rails 8 renamed Rails::Rack::Logger to ActionDispatch::Request::Logger
8
+ # Determine which logger middleware class to use
9
+ logger_class = if Object.const_defined?("ActionDispatch::Request::Logger")
10
+ ActionDispatch::Request::Logger
11
+ elsif Object.const_defined?("Rails::Rack::Logger")
12
+ Rails::Rack::Logger
13
+ else
14
+ # If neither exists, skip the middleware swap
15
+ nil
16
+ end
17
+
18
+ if logger_class
19
+ begin
20
+ config.middleware.swap(
21
+ logger_class,
22
+ Silencer::Logger,
23
+ config.log_tags,
24
+ silence: ["/up"]
25
+ )
26
+ rescue RuntimeError => e
27
+ # Silently fail if middleware doesn't exist in stack
28
+ raise unless e.message.include?("No such middleware")
29
+ end
30
+ end
31
+ end
@@ -1,15 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Panda::CMS.configure do |config|
4
- # The main title of your website
5
- config.title = "Demo Site"
6
-
7
- # Site access control
8
- config.require_login_to_view = false
9
- end
10
-
11
- # Admin path is now configured via Panda::Core
3
+ # This file is an example of Panda configuration.
4
+ # In your application, this should be at config/initializers/panda.rb
12
5
  Panda::Core.configure do |config|
13
- # The path to the administration panel
14
6
  config.admin_path = "/admin"
7
+
8
+ config.login_page_title = "Panda Admin"
9
+
10
+ # Configure authentication providers
11
+ # Uncomment and configure the providers you want to use
12
+ # Don't forget to add the corresponding gems (e.g., omniauth-google-oauth2)
13
+ #
14
+ # config.authentication_providers = {
15
+ # google_oauth2: {
16
+ # enabled: true,
17
+ # name: "Google", # Display name for the button
18
+ # client_id: Rails.application.credentials.dig(:google, :client_id),
19
+ # client_secret: Rails.application.credentials.dig(:google, :client_secret),
20
+ # options: {
21
+ # scope: "email,profile",
22
+ # prompt: "select_account",
23
+ # hd: "yourdomain.com" # Specify your domain here if you want to restrict admin logins
24
+ # }
25
+ # }
26
+ # }
27
+
28
+ # Configure the session token cookie name
29
+ config.session_token_cookie = :panda_session
30
+
31
+ # Configure the user class for the application
32
+ config.user_class = "Panda::Core::User"
33
+
34
+ # Configure the user identity class for the application
35
+ config.user_identity_class = "Panda::Core::UserIdentity"
36
+
37
+ # Configure the storage provider (default: :active_storage)
38
+ # config.storage_provider = :active_storage
39
+
40
+ # Configure the cache store (default: :memory_store)
41
+ # config.cache_store = :memory_store
15
42
  end
43
+
44
+ # Optional CMS-specific configuration
45
+ # Panda::CMS.configure do |config|
46
+ # # Site access control
47
+ # config.require_login_to_view = false
48
+ # end
49
+
50
+ # Optional EditorJS configuration
51
+ # Panda::Editor.configure do |config|
52
+ # # Additional EditorJS tools to load
53
+ # # config.editor_js_tools = []
54
+ #
55
+ # # EditorJS tool configurations
56
+ # # config.editor_js_tool_config = {}
57
+ # end
@@ -29,6 +29,17 @@ en:
29
29
  title: Title
30
30
  path: URL
31
31
  panda_cms_template_id: Template
32
+ page_type: Page Type
33
+ seo_title: SEO Title
34
+ seo_description: SEO Description
35
+ seo_keywords: SEO Keywords
36
+ seo_index_mode: Search Engine Visibility
37
+ canonical_url: Canonical URL
38
+ og_title: OpenGraph Title
39
+ og_description: OpenGraph Description
40
+ og_type: OpenGraph Type
41
+ og_image: OpenGraph Image
42
+ inherit_seo: Inherit Settings
32
43
  panda/cms/post:
33
44
  title: Title
34
45
  slug: URL
@@ -36,6 +47,15 @@ en:
36
47
  user_id: Author
37
48
  published_at: Published At
38
49
  post_content: Content
50
+ seo_title: SEO Title
51
+ seo_description: SEO Description
52
+ seo_keywords: SEO Keywords
53
+ seo_index_mode: Search Engine Visibility
54
+ canonical_url: Canonical URL
55
+ og_title: OpenGraph Title
56
+ og_description: OpenGraph Description
57
+ og_type: OpenGraph Type
58
+ og_image: OpenGraph Image
39
59
  statuses:
40
60
  active: Active
41
61
  draft: Draft
@@ -52,3 +72,24 @@ en:
52
72
  lastname: Last Name
53
73
  email: Email Address
54
74
  current_theme: Theme
75
+ enums:
76
+ panda/cms/page:
77
+ seo_index_mode:
78
+ visible: Visible to search engines
79
+ invisible: Hidden from search engines
80
+ og_type:
81
+ website: Website
82
+ article: Article
83
+ profile: Profile
84
+ video: Video
85
+ book: Book
86
+ panda/cms/post:
87
+ seo_index_mode:
88
+ visible: Visible to search engines
89
+ invisible: Hidden from search engines
90
+ og_type:
91
+ website: Website
92
+ article: Article
93
+ profile: Profile
94
+ video: Video
95
+ book: Book
data/config/routes.rb CHANGED
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Panda::CMS::Engine.routes.draw do
4
- constraints Panda::Core::AdminConstraint.new(&:present?) do
4
+ # Test authentication endpoint moved to panda-core at /admin/test_login/:user_id
5
+
6
+ constraints Panda::Core::AdminConstraint.new do
5
7
  # CMS-specific dashboard (using Core's admin_path)
6
- admin_path = Panda::Core.configuration.admin_path
8
+ admin_path = Panda::Core.config.admin_path
7
9
  get "#{admin_path}/cms", to: "admin/dashboard#show", as: :admin_cms_dashboard
8
10
 
9
11
  namespace admin_path.delete_prefix("/").to_sym, path: "#{admin_path}/cms", as: :admin_cms, module: :admin do
10
12
  resources :files
11
- resources :forms, only: %i[index show]
13
+ resources :forms
12
14
  resources :menus
13
15
  resources :pages do
14
16
  resources :block_contents, only: %i[update]
@@ -4,12 +4,12 @@ class ConvertHtmlContentToEditorJs < ActiveRecord::Migration[7.1]
4
4
  def up
5
5
  # First, let's check if the converter service exists
6
6
  converter_path = Panda::CMS::Engine.root.join("app/services/panda/cms/html_to_editor_js_converter.rb")
7
-
7
+
8
8
  unless File.exist?(converter_path)
9
9
  Rails.logger.info "HtmlToEditorJsConverter service not found. Skipping HTML to EditorJS conversion."
10
10
  return
11
11
  end
12
-
12
+
13
13
  require converter_path
14
14
 
15
15
  # Check if we have any existing valid EditorJS content
@@ -8,7 +8,12 @@ class AddNestedSetsToPandaCMSPages < ActiveRecord::Migration[7.1]
8
8
 
9
9
  # This is necessary to update :lft and :rgt columns
10
10
  Panda::CMS::Page.reset_column_information
11
- Panda::CMS::Page.rebuild!
11
+
12
+ # Only rebuild if there are existing pages
13
+ # On fresh installs, there won't be any pages yet
14
+ if Panda::CMS::Page.any?
15
+ Panda::CMS::Page.rebuild!
16
+ end
12
17
  end
13
18
 
14
19
  def self.down
@@ -7,27 +7,29 @@ class MigrateUsersToPandaCore < ActiveRecord::Migration[8.0]
7
7
  if table_exists?(:panda_cms_users) && table_exists?(:panda_core_users)
8
8
  # Check if there's any data to migrate
9
9
  cms_user_count = ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM panda_cms_users")
10
- return if cms_user_count == 0
11
- # Copy all user data
12
- execute <<-SQL
13
- INSERT INTO panda_core_users (
14
- id, name, email, image_url, is_admin, created_at, updated_at
15
- )
16
- SELECT
17
- id,
18
- COALESCE(name, CONCAT(firstname, ' ', lastname), 'Unknown User'),
19
- email,
20
- image_url,
21
- COALESCE(admin, false),
22
- created_at,
23
- updated_at
24
- FROM panda_cms_users
25
- WHERE NOT EXISTS (
26
- SELECT 1 FROM panda_core_users WHERE panda_core_users.id = panda_cms_users.id
27
- )
28
- SQL
29
10
 
30
- # Update foreign key references in other tables
11
+ if cms_user_count > 0
12
+ # Copy all user data
13
+ execute <<-SQL
14
+ INSERT INTO panda_core_users (
15
+ id, name, email, image_url, is_admin, created_at, updated_at
16
+ )
17
+ SELECT
18
+ id,
19
+ COALESCE(name, CONCAT(firstname, ' ', lastname), 'Unknown User'),
20
+ email,
21
+ image_url,
22
+ COALESCE(admin, false),
23
+ created_at,
24
+ updated_at
25
+ FROM panda_cms_users
26
+ WHERE NOT EXISTS (
27
+ SELECT 1 FROM panda_core_users WHERE panda_core_users.id = panda_cms_users.id
28
+ )
29
+ SQL
30
+ end
31
+
32
+ # Update foreign key references in other tables (always do this, even if no data)
31
33
 
32
34
  # Posts author_id
33
35
  if column_exists?(:panda_cms_posts, :author_id)
@@ -47,7 +49,7 @@ class MigrateUsersToPandaCore < ActiveRecord::Migration[8.0]
47
49
  add_foreign_key :panda_cms_visits, :panda_core_users, column: :user_id, primary_key: :id
48
50
  end
49
51
 
50
- # Drop the old table
52
+ # Always drop the old table if it exists (even if it was empty)
51
53
  drop_table :panda_cms_users
52
54
  end
53
55
  end
@@ -0,0 +1,22 @@
1
+ class AddCachedLastUpdatedAtToPandaCMSPages < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :panda_cms_pages, :cached_last_updated_at, :datetime
4
+ add_index :panda_cms_pages, :cached_last_updated_at
5
+
6
+ # Backfill existing pages
7
+ reversible do |dir|
8
+ dir.up do
9
+ execute <<-SQL
10
+ UPDATE panda_cms_pages
11
+ SET cached_last_updated_at = GREATEST(
12
+ updated_at,
13
+ COALESCE(
14
+ (SELECT MAX(updated_at) FROM panda_cms_block_contents WHERE panda_cms_page_id = panda_cms_pages.id),
15
+ updated_at
16
+ )
17
+ )
18
+ SQL
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ class AddPageTypeToPandaCMSPages < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :panda_cms_pages, :page_type, :string, default: "standard", null: false
4
+ add_index :panda_cms_pages, :page_type
5
+ end
6
+ end
@@ -0,0 +1,27 @@
1
+ class SetPageTypesForExistingPages < ActiveRecord::Migration[8.0]
2
+ def up
3
+ # Set system type for error pages
4
+ execute <<-SQL
5
+ UPDATE panda_cms_pages
6
+ SET page_type = 'system'
7
+ WHERE path IN ('/404', '/500')
8
+ SQL
9
+
10
+ # Set posts type for news/blog pages
11
+ execute <<-SQL
12
+ UPDATE panda_cms_pages
13
+ SET page_type = 'posts'
14
+ WHERE path LIKE '%news%' OR path LIKE '%blog%' OR path LIKE '%updates%'
15
+ SQL
16
+
17
+ # All other pages remain as 'standard' (the default)
18
+ end
19
+
20
+ def down
21
+ # Reset all to standard
22
+ execute <<-SQL
23
+ UPDATE panda_cms_pages
24
+ SET page_type = 'standard'
25
+ SQL
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddPendingReviewStatusToPagesAndPosts < ActiveRecord::Migration[8.0]
4
+ def up
5
+ # Add pending_review status to pages and posts for content approval workflow
6
+ # This allows content creators to submit drafts for editorial review
7
+
8
+ # Note: We're not modifying the enum directly in the database
9
+ # Rails enums are string-based, so we just document the new allowed value
10
+ # The application code will handle the new status value
11
+
12
+ # Update any existing pages/posts that might benefit from this status
13
+ # (In a real migration, you'd add logic here if needed)
14
+ end
15
+
16
+ def down
17
+ # Convert any pending_review items back to draft
18
+ Panda::CMS::Page.where(status: "pending_review").update_all(status: "draft")
19
+ Panda::CMS::Post.where(status: "pending_review").update_all(status: "draft")
20
+ end
21
+ end
@@ -0,0 +1,32 @@
1
+ class AddSEOFieldsToPages < ActiveRecord::Migration[8.0]
2
+ def change
3
+ # Create enum types for SEO fields
4
+ create_enum :panda_cms_seo_index_mode, ["visible", "invisible"]
5
+ create_enum :panda_cms_og_type, ["website", "article", "profile", "video", "book"]
6
+
7
+ # Add SEO basic fields
8
+ add_column :panda_cms_pages, :seo_title, :string
9
+ add_column :panda_cms_pages, :seo_description, :text
10
+ add_column :panda_cms_pages, :seo_keywords, :string
11
+
12
+ # Robots control (visible/invisible in base CMS)
13
+ add_column :panda_cms_pages, :seo_index_mode, :enum,
14
+ enum_type: "panda_cms_seo_index_mode",
15
+ default: "visible",
16
+ null: false
17
+
18
+ # Canonical URL
19
+ add_column :panda_cms_pages, :canonical_url, :string
20
+
21
+ # OpenGraph / Social Sharing
22
+ add_column :panda_cms_pages, :og_title, :string
23
+ add_column :panda_cms_pages, :og_description, :text
24
+ add_column :panda_cms_pages, :og_type, :enum,
25
+ enum_type: "panda_cms_og_type",
26
+ default: "website",
27
+ null: false
28
+
29
+ # Inheritance (pages only - they have hierarchy via nested sets)
30
+ add_column :panda_cms_pages, :inherit_seo, :boolean, default: true, null: false
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ class AddSEOFieldsToPosts < ActiveRecord::Migration[8.0]
2
+ def change
3
+ # Add SEO basic fields
4
+ add_column :panda_cms_posts, :seo_title, :string
5
+ add_column :panda_cms_posts, :seo_description, :text
6
+ add_column :panda_cms_posts, :seo_keywords, :string
7
+
8
+ # Robots control (visible/invisible in base CMS)
9
+ add_column :panda_cms_posts, :seo_index_mode, :enum,
10
+ enum_type: "panda_cms_seo_index_mode",
11
+ default: "visible",
12
+ null: false
13
+
14
+ # Canonical URL
15
+ add_column :panda_cms_posts, :canonical_url, :string
16
+
17
+ # OpenGraph / Social Sharing
18
+ add_column :panda_cms_posts, :og_title, :string
19
+ add_column :panda_cms_posts, :og_description, :text
20
+ add_column :panda_cms_posts, :og_type, :enum,
21
+ enum_type: "panda_cms_og_type",
22
+ default: "article", # Posts default to 'article' type
23
+ null: false
24
+
25
+ # Note: No inherit_seo for posts - they don't have hierarchy
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ class AddSpamTrackingToFormSubmissions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :panda_cms_form_submissions, :ip_address, :string
4
+ add_column :panda_cms_form_submissions, :user_agent, :text
5
+ add_index :panda_cms_form_submissions, :ip_address
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddPerformanceIndexesToPagesAndRedirects < ActiveRecord::Migration[8.0]
4
+ def change
5
+ # Add index to pages.path for fast page lookups
6
+ # This is the most critical query path in the CMS
7
+ add_index :panda_cms_pages, :path, name: "index_panda_cms_pages_on_path"
8
+
9
+ # Add index to redirects.origin_path for fast redirect lookups
10
+ # Checked on every request in PagesController#handle_redirects
11
+ add_index :panda_cms_redirects, :origin_path, name: "index_panda_cms_redirects_on_origin_path"
12
+ end
13
+ end
@@ -10,11 +10,8 @@ module Generators
10
10
  desc "Adds the basic configuration for Panda CMS to your Rails app."
11
11
 
12
12
  def create_initializer_file
13
- # Add the initializer
14
- initializer_path = "config/initializers/panda/cms.rb"
15
- unless File.exist?("#{::Rails.root}/#{initializer_path}")
16
- FileUtils.cp "#{::Panda::CMS::Engine.root}/#{initializer_path}", "#{::Rails.root}/#{initializer_path}"
17
- end
13
+ # Skip creating initializer - Panda::Core already creates config/initializers/panda.rb
14
+ # See config/initializers/panda/cms.rb in the gem for an example configuration
18
15
 
19
16
  # Add the seed loader to the seeds.rb file
20
17
  unless File.read("#{::Rails.root}/db/seeds.rb")&.include?("Panda::CMS::Engine.load_seed")
@@ -35,11 +35,9 @@ module Panda
35
35
 
36
36
  # Check if GitHub-hosted assets should be used
37
37
  def use_github_assets?
38
- # Use GitHub assets in production or when explicitly enabled
39
- Rails.env.production? ||
40
- ENV["PANDA_CMS_USE_GITHUB_ASSETS"] == "true" ||
41
- !development_assets_available? ||
42
- ((Rails.env.test? || in_test_environment?) && compiled_assets_available?)
38
+ # Panda CMS uses importmaps for JavaScript (no compilation needed)
39
+ # Only use GitHub assets in production or when explicitly enabled
40
+ false # Always use importmaps like panda-core
43
41
  end
44
42
 
45
43
  # Download assets from GitHub to local cache
@@ -105,39 +103,10 @@ module Panda
105
103
  end
106
104
 
107
105
  def development_asset_tags(options = {})
108
- # In test environment with CI, always use compiled assets
109
- if (Rails.env.test? || ENV["CI"].present?) && compiled_assets_available?
110
- # Use the same logic as GitHub assets but with local paths
111
- version = asset_version
112
- js_url = "/panda-cms-assets/panda-cms-#{version}.js"
113
- css_url = "/panda-cms-assets/panda-cms-#{version}.css"
114
-
115
- tags = []
116
-
117
- # JavaScript tag
118
- tags << content_tag(:script, "", {
119
- src: js_url,
120
- defer: true
121
- })
122
-
123
- # CSS tag if exists
124
- if cached_asset_exists?(css_url)
125
- tags << tag(:link, {
126
- rel: "stylesheet",
127
- href: css_url
128
- })
129
- end
130
-
131
- tags.join("\n").html_safe
132
- else
133
- # In development, just use a simple script tag
134
- # The view will handle importmap tags separately
135
- content_tag(:script, "", {
136
- src: development_javascript_url,
137
- type: "module",
138
- defer: true
139
- })
140
- end
106
+ # Panda CMS uses importmaps for JavaScript (no compiled bundles)
107
+ # The view will handle importmap tags via <%= javascript_importmap_tags %>
108
+ # We don't need to return anything here - CSS comes from panda-core
109
+ "".html_safe
141
110
  end
142
111
 
143
112
  def github_javascript_url
@@ -161,20 +130,8 @@ module Panda
161
130
  end
162
131
 
163
132
  def development_javascript_url
164
- # Try cached assets first, then importmap
165
- version = asset_version
166
- # Try root level first (standalone bundle), then versioned directory
167
- root_path = "/panda-cms-assets/panda-cms-#{version}.js"
168
- versioned_path = "/panda-cms-assets/#{version}/panda-cms-#{version}.js"
169
-
170
- if cached_asset_exists?(root_path)
171
- root_path
172
- elsif cached_asset_exists?(versioned_path)
173
- versioned_path
174
- else
175
- # Fallback to importmap or engine asset
176
- "/assets/panda/cms/controllers/index.js"
177
- end
133
+ # Always use importmap (no compiled bundles)
134
+ "/panda/cms/application.js"
178
135
  end
179
136
 
180
137
  def development_css_url
@@ -202,14 +159,7 @@ module Panda
202
159
  end
203
160
 
204
161
  def asset_version
205
- # In test environment, use VERSION constant for consistency with compiled assets
206
- # In other environments, use git SHA for dynamic versioning
207
- # Also check for test environment indicators since Rails.env might be development in specs
208
- if Rails.env.test? || ENV["CI"].present? || in_test_environment?
209
- Panda::CMS::VERSION
210
- else
211
- `git rev-parse --short HEAD`.strip
212
- end
162
+ Panda::CMS::VERSION
213
163
  end
214
164
 
215
165
  def in_test_environment?
@@ -217,14 +167,6 @@ module Panda
217
167
  defined?(RSpec) && RSpec.respond_to?(:configuration)
218
168
  end
219
169
 
220
- def compiled_assets_available?
221
- # Check if compiled assets exist in test location
222
- version = asset_version
223
- js_file = Rails.public_path.join("panda-cms-assets", "panda-cms-#{version}.js")
224
- css_file = Rails.public_path.join("panda-cms-assets", "panda-cms-#{version}.css")
225
- js_file.exist? && css_file.exist?
226
- end
227
-
228
170
  def development_assets_available?
229
171
  # Check if local development assets exist (importmap, etc.)
230
172
  importmap_available? || engine_assets_available?
@@ -234,12 +176,18 @@ module Panda
234
176
  return false unless defined?(Rails.application.importmap)
235
177
 
236
178
  begin
237
- # Rails 8+ uses a different API for accessing importmap entries
238
- if Rails.application.importmap.respond_to?(:to_json)
239
- importmap_json = JSON.parse(Rails.application.importmap.to_json)
240
- importmap_json.any? { |name, _| name.include?("panda") }
179
+ # Try different API methods depending on importmap-rails version
180
+ if Rails.application.importmap.respond_to?(:packages)
181
+ # importmap-rails 2.x - check packages hash
182
+ Rails.application.importmap.packages.keys.any? { |name| name.to_s.include?("panda") }
241
183
  elsif Rails.application.importmap.respond_to?(:entries)
184
+ # importmap-rails 1.x - check entries array
242
185
  Rails.application.importmap.entries.any? { |entry| entry.name.include?("panda") }
186
+ elsif Rails.application.importmap.respond_to?(:to_json)
187
+ # Fallback with proper resolver
188
+ # Note: to_json requires a resolver in newer versions
189
+ # We'll just return false if we can't access packages/entries
190
+ false
243
191
  else
244
192
  false
245
193
  end
@@ -251,10 +199,17 @@ module Panda
251
199
 
252
200
  def engine_assets_available?
253
201
  # Check if engine's JavaScript files are available
254
- engine_js_path = Rails.root.join("..", "..", "app", "javascript", "panda", "cms", "controllers", "index.js")
202
+ engine_js_path = Panda::CMS::Engine.root.join("app", "javascript", "panda", "cms", "controllers", "index.js")
255
203
  File.exist?(engine_js_path)
256
204
  end
257
205
 
206
+ def compiled_assets_available?
207
+ # Check if compiled JavaScript bundle exists
208
+ version = asset_version
209
+ js_file = Rails.public_path.join("panda-cms-assets", "panda-cms-#{version}.js")
210
+ js_file.exist?
211
+ end
212
+
258
213
  def cached_assets_exist?(version)
259
214
  cache_dir = local_cache_directory.join(version)
260
215
  cache_dir.exist? && cache_dir.join("panda-cms-#{version}.js").exist?
@@ -312,6 +267,21 @@ module Panda
312
267
  end
313
268
  end
314
269
 
270
+ # Simple response object for HTTP requests
271
+ class Response
272
+ attr_reader :code, :body
273
+
274
+ def initialize(success:, code:, body:)
275
+ @success = success
276
+ @code = code
277
+ @body = body
278
+ end
279
+
280
+ def success?
281
+ @success
282
+ end
283
+ end
284
+
315
285
  def fetch_url(url)
316
286
  uri = URI(url)
317
287
  http = Net::HTTP.new(uri.host, uri.port)
@@ -324,14 +294,14 @@ module Panda
324
294
 
325
295
  response = http.request(request)
326
296
 
327
- OpenStruct.new(
328
- success?: response.code.to_i == 200,
297
+ Response.new(
298
+ success: response.code.to_i == 200,
329
299
  code: response.code,
330
300
  body: response.body
331
301
  )
332
302
  rescue => e
333
303
  Rails.logger.error "[Panda CMS] Network error: #{e.message}"
334
- OpenStruct.new(success?: false, code: "error", body: nil)
304
+ Response.new(success: false, code: "error", body: nil)
335
305
  end
336
306
 
337
307
  def asset_integrity(version, filename)