rails_site_engine 0.6.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 (110) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +137 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/stylesheets/rails_site_engine/application.css +15 -0
  6. data/app/assets/stylesheets/rails_site_engine/engine.css +2 -0
  7. data/app/assets/stylesheets/rails_site_engine/engine.tailwind.css +5 -0
  8. data/app/controllers/rails_site_engine/application_controller.rb +27 -0
  9. data/app/controllers/rails_site_engine/contacts_controller.rb +35 -0
  10. data/app/controllers/rails_site_engine/meta_controller.rb +20 -0
  11. data/app/controllers/rails_site_engine/pages_controller.rb +18 -0
  12. data/app/helpers/rails_site_engine/application_helper.rb +266 -0
  13. data/app/javascript/controllers/rails_site_engine/color_mode_controller.js +83 -0
  14. data/app/javascript/controllers/rails_site_engine/form_submit_controller.js +22 -0
  15. data/app/javascript/controllers/rails_site_engine/mobile_nav_controller.js +45 -0
  16. data/app/javascript/rails_site_engine/application.js +8 -0
  17. data/app/jobs/rails_site_engine/application_job.rb +4 -0
  18. data/app/mailers/rails_site_engine/application_mailer.rb +6 -0
  19. data/app/mailers/rails_site_engine/contact_mailer.rb +25 -0
  20. data/app/models/rails_site_engine/application_record.rb +5 -0
  21. data/app/models/rails_site_engine/contact_message.rb +16 -0
  22. data/app/views/layouts/mailer.html.erb +13 -0
  23. data/app/views/layouts/mailer.text.erb +1 -0
  24. data/app/views/layouts/rails_site_engine/application.html.erb +54 -0
  25. data/app/views/rails_site_engine/contact_mailer/notify.text.erb +9 -0
  26. data/app/views/rails_site_engine/contacts/new.html.erb +1 -0
  27. data/app/views/rails_site_engine/pages/show.html.erb +15 -0
  28. data/app/views/rails_site_engine/sections/_contact_form.html.erb +112 -0
  29. data/app/views/rails_site_engine/sections/_cta_band.html.erb +27 -0
  30. data/app/views/rails_site_engine/sections/_faq.html.erb +29 -0
  31. data/app/views/rails_site_engine/sections/_feature_cards.html.erb +30 -0
  32. data/app/views/rails_site_engine/sections/_hero.html.erb +39 -0
  33. data/app/views/rails_site_engine/sections/_pricing_cards.html.erb +53 -0
  34. data/app/views/rails_site_engine/sections/_process_steps.html.erb +35 -0
  35. data/app/views/rails_site_engine/sections/_project_cards.html.erb +48 -0
  36. data/app/views/rails_site_engine/sections/_proof_strip.html.erb +16 -0
  37. data/app/views/rails_site_engine/sections/_rich_text.html.erb +15 -0
  38. data/app/views/rails_site_engine/sections/_stats.html.erb +34 -0
  39. data/app/views/rails_site_engine/sections/_testimonials.html.erb +40 -0
  40. data/app/views/rails_site_engine/sections/_unknown.html.erb +6 -0
  41. data/app/views/rails_site_engine/shared/_flash.html.erb +18 -0
  42. data/app/views/rails_site_engine/shared/_footer.html.erb +41 -0
  43. data/app/views/rails_site_engine/shared/_navbar.html.erb +118 -0
  44. data/config/importmap.rb +5 -0
  45. data/config/locales/en.yml +27 -0
  46. data/config/routes.rb +9 -0
  47. data/config/site_profiles.yml +23 -0
  48. data/lib/generators/rails_site_engine/install/install_generator.rb +454 -0
  49. data/lib/generators/rails_site_engine/install/templates/config/theme.yml +3 -0
  50. data/lib/generators/rails_site_engine/install/templates/content/pages/about.md +12 -0
  51. data/lib/generators/rails_site_engine/install/templates/content/pages/contact.md +13 -0
  52. data/lib/generators/rails_site_engine/install/templates/content/pages/home.md +39 -0
  53. data/lib/generators/rails_site_engine/install/templates/content/pages/privacy.md +43 -0
  54. data/lib/generators/rails_site_engine/install/templates/content/pages/services.md +25 -0
  55. data/lib/generators/rails_site_engine/install/templates/content/site.yml +36 -0
  56. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/config/theme.yml +3 -0
  57. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/about.md +14 -0
  58. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/contact.md +12 -0
  59. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/home.md +55 -0
  60. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/privacy.md +43 -0
  61. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/services.md +29 -0
  62. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/site.yml +36 -0
  63. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/config/theme.yml +3 -0
  64. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/about.md +14 -0
  65. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/contact.md +12 -0
  66. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/home.md +55 -0
  67. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/privacy.md +43 -0
  68. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/services.md +29 -0
  69. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/site.yml +36 -0
  70. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/config/theme.yml +3 -0
  71. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/about.md +14 -0
  72. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/contact.md +12 -0
  73. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/home.md +55 -0
  74. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/privacy.md +43 -0
  75. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/services.md +29 -0
  76. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/site.yml +36 -0
  77. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/config/theme.yml +3 -0
  78. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/about.md +14 -0
  79. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/contact.md +12 -0
  80. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/home.md +55 -0
  81. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/privacy.md +43 -0
  82. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/services.md +29 -0
  83. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/site.yml +36 -0
  84. data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/config/theme.yml +3 -0
  85. data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/about.md +26 -0
  86. data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/contact.md +12 -0
  87. data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/home.md +43 -0
  88. data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/privacy.md +43 -0
  89. data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/work.md +27 -0
  90. data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/site.yml +36 -0
  91. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/config/theme.yml +3 -0
  92. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/about.md +14 -0
  93. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/contact.md +12 -0
  94. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/home.md +48 -0
  95. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/pricing.md +57 -0
  96. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/privacy.md +43 -0
  97. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/projects.md +27 -0
  98. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/site.yml +38 -0
  99. data/lib/rails_site_engine/content/store.rb +297 -0
  100. data/lib/rails_site_engine/engine.rb +15 -0
  101. data/lib/rails_site_engine/markdown_renderer.rb +37 -0
  102. data/lib/rails_site_engine/profile_defaults.rb +83 -0
  103. data/lib/rails_site_engine/site_engine_config.rb +78 -0
  104. data/lib/rails_site_engine/site_profiles.rb +166 -0
  105. data/lib/rails_site_engine/sitemap_xml.rb +26 -0
  106. data/lib/rails_site_engine/theme.rb +54 -0
  107. data/lib/rails_site_engine/version.rb +3 -0
  108. data/lib/rails_site_engine.rb +13 -0
  109. data/lib/tasks/rails_site_engine_tasks.rake +7 -0
  110. metadata +179 -0
@@ -0,0 +1,118 @@
1
+ <div
2
+ class="mx-auto w-full max-w-6xl px-4"
3
+ data-controller="rails-site-engine--mobile-nav"
4
+ data-action="keydown.esc@window->rails-site-engine--mobile-nav#close"
5
+ >
6
+ <div class="navbar min-h-20 px-0">
7
+ <div class="navbar-start gap-2">
8
+ <button
9
+ type="button"
10
+ class="btn btn-ghost btn-square lg:hidden"
11
+ aria-label="<%= t("rails_site_engine.nav.open_menu") %>"
12
+ aria-expanded="false"
13
+ data-action="click->rails-site-engine--mobile-nav#toggle"
14
+ data-rails-site-engine--mobile-nav-target="toggleButton"
15
+ >
16
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
17
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
18
+ </svg>
19
+ </button>
20
+
21
+ <%= link_to brand_label, root_path, class: "btn btn-ghost px-2 text-lg font-semibold tracking-tight" %>
22
+ </div>
23
+
24
+ <div class="navbar-center hidden lg:flex">
25
+ <ul class="menu menu-horizontal gap-1 px-1">
26
+ <% primary_nav_items.each do |item| %>
27
+ <li>
28
+ <%= link_to item[:label], item[:href], **nav_link_options(item) %>
29
+ </li>
30
+ <% end %>
31
+ </ul>
32
+ </div>
33
+
34
+ <div class="navbar-end gap-2">
35
+ <% if theme.daisyui_dark_mode_enabled? %>
36
+ <button
37
+ type="button"
38
+ class="btn btn-ghost btn-circle"
39
+ aria-label="<%= t("rails_site_engine.nav.toggle_color_mode") %>"
40
+ data-action="click->rails-site-engine--color-mode#toggle"
41
+ data-rails-site-engine--color-mode-target="toggleButton"
42
+ >
43
+ <svg class="h-5 w-5" data-rails-site-engine--color-mode-target="lightIcon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
44
+ <path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12Zm0-14a1 1 0 0 1-1-1V2a1 1 0 1 1 2 0v1a1 1 0 0 1-1 1Zm0 20a1 1 0 0 1-1-1v-1a1 1 0 1 1 2 0v1a1 1 0 0 1-1 1ZM4 13H3a1 1 0 1 1 0-2h1a1 1 0 1 1 0 2Zm18 0h-1a1 1 0 1 1 0-2h1a1 1 0 1 1 0 2ZM5.636 6.636a1 1 0 0 1-1.414 0l-.707-.707A1 1 0 1 1 4.93 4.515l.707.707a1 1 0 0 1 0 1.414Zm14.849 14.849a1 1 0 0 1-1.414 0l-.707-.707a1 1 0 1 1 1.414-1.414l.707.707a1 1 0 0 1 0 1.414ZM18.364 6.636a1 1 0 0 1 0-1.414l.707-.707a1 1 0 0 1 1.414 1.414l-.707.707a1 1 0 0 1-1.414 0ZM3.515 21.485a1 1 0 0 1 0-1.414l.707-.707a1 1 0 0 1 1.414 1.414l-.707.707a1 1 0 0 1-1.414 0Z"/>
45
+ </svg>
46
+
47
+ <svg class="hidden h-5 w-5" data-rails-site-engine--color-mode-target="darkIcon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
48
+ <path d="M21.752 15.002A9.718 9.718 0 0 1 12.003 22C6.487 22 2 17.514 2 12c0-4.287 2.66-8.06 6.65-9.42a1 1 0 0 1 1.247 1.246A7.495 7.495 0 0 0 9 7.5c0 4.136 3.364 7.5 7.5 7.5 1.3 0 2.53-.332 3.605-.918a1 1 0 0 1 1.647.92Z"/>
49
+ </svg>
50
+ </button>
51
+ <% end %>
52
+
53
+ <% if (cta = primary_cta).present? %>
54
+ <%= link_to cta[:label], cta[:href], class: "btn btn-primary hidden sm:inline-flex", target: (cta[:external] ? "_blank" : nil), rel: (cta[:external] ? "noopener noreferrer" : nil) %>
55
+ <% end %>
56
+ </div>
57
+ </div>
58
+
59
+ <div
60
+ class="fixed inset-0 z-50 hidden lg:hidden"
61
+ role="dialog"
62
+ aria-modal="true"
63
+ aria-label="<%= t("rails_site_engine.nav.mobile_menu") %>"
64
+ data-rails-site-engine--mobile-nav-target="panel"
65
+ >
66
+ <button
67
+ type="button"
68
+ class="absolute inset-0 bg-base-content/40"
69
+ aria-label="<%= t("rails_site_engine.nav.close_menu") %>"
70
+ data-action="click->rails-site-engine--mobile-nav#close"
71
+ ></button>
72
+
73
+ <aside class="relative h-full w-80 max-w-[85vw] border-r border-base-200 bg-base-100 p-4 shadow-2xl">
74
+ <div class="mb-5 flex items-center justify-between">
75
+ <div class="text-lg font-semibold"><%= brand_label %></div>
76
+ <button
77
+ type="button"
78
+ class="btn btn-ghost btn-square"
79
+ aria-label="<%= t("rails_site_engine.nav.close_menu") %>"
80
+ data-action="click->rails-site-engine--mobile-nav#close"
81
+ >
82
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
83
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
84
+ </svg>
85
+ </button>
86
+ </div>
87
+
88
+ <ul class="menu gap-1">
89
+ <% primary_nav_items.each do |item| %>
90
+ <li>
91
+ <%= link_to item[:label], item[:href], **nav_link_options(item, data: { action: "click->rails-site-engine--mobile-nav#close" }) %>
92
+ </li>
93
+ <% end %>
94
+ </ul>
95
+
96
+ <% if (cta = primary_cta).present? %>
97
+ <div class="mt-6">
98
+ <%= link_to cta[:label], cta[:href], class: "btn btn-primary btn-block", target: (cta[:external] ? "_blank" : nil), rel: (cta[:external] ? "noopener noreferrer" : nil), data: { action: "click->rails-site-engine--mobile-nav#close" } %>
99
+ </div>
100
+ <% end %>
101
+
102
+ <% if contact_channels.any? %>
103
+ <div class="mt-8 border-t border-base-200 pt-4 text-sm">
104
+ <% if (phone = contact_channels[:phone]).present? %>
105
+ <p>
106
+ <%= link_to phone, "tel:#{phone}", class: "link link-hover" %>
107
+ </p>
108
+ <% end %>
109
+ <% if (email = contact_channels[:email]).present? %>
110
+ <p>
111
+ <%= link_to email, "mailto:#{email}", class: "link link-hover" %>
112
+ </p>
113
+ <% end %>
114
+ </div>
115
+ <% end %>
116
+ </aside>
117
+ </div>
118
+ </div>
@@ -0,0 +1,5 @@
1
+ pin "@hotwired/stimulus", to: "stimulus.min.js"
2
+ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
3
+ pin "rails_site_engine/application", to: "rails_site_engine/application.js"
4
+
5
+ pin_all_from RailsSiteEngine::Engine.root.join("app/javascript/controllers"), under: "rails_site_engine/controllers", to: "controllers"
@@ -0,0 +1,27 @@
1
+ en:
2
+ rails_site_engine:
3
+ a11y:
4
+ skip_to_main: "Skip to main content"
5
+ nav:
6
+ open_menu: "Open navigation menu"
7
+ close_menu: "Close navigation menu"
8
+ mobile_menu: "Mobile navigation"
9
+ toggle_color_mode: "Toggle color mode"
10
+ footer:
11
+ navigation: "Footer navigation"
12
+ quick_links: "Quick links"
13
+ contact: "Contact"
14
+ back_to_top: "Back to top"
15
+ contact:
16
+ heading_fallback: "Contact"
17
+ form_title: "Send a message"
18
+ errors_title: "Please fix the following:"
19
+ success_notice: "Thanks! We'll be in touch shortly."
20
+ invalid_notice: "Please fix the errors below."
21
+ submit: "Send"
22
+ submitting: "Sending..."
23
+ fields:
24
+ name: "Name"
25
+ email: "Email"
26
+ phone: "Phone (optional)"
27
+ message: "Message"
data/config/routes.rb ADDED
@@ -0,0 +1,9 @@
1
+ RailsSiteEngine::Engine.routes.draw do
2
+ root to: "pages#show"
3
+
4
+ get "/robots.txt", to: "meta#robots", defaults: { format: :text }
5
+ get "/sitemap.xml", to: "meta#sitemap", defaults: { format: :xml }
6
+
7
+ get "/*path", to: "pages#show", as: :page
8
+ post "/*path", to: "contacts#create", as: :contact
9
+ end
@@ -0,0 +1,23 @@
1
+ profiles:
2
+ home-services:
3
+ label: "Home services"
4
+ default_specialty: "general"
5
+ specialties:
6
+ general:
7
+ label: "General home services"
8
+ template: "home-services-general"
9
+ plumbing:
10
+ label: "Plumbing services"
11
+ template: "home-services-plumbing"
12
+ carpentry:
13
+ label: "Carpentry services"
14
+ template: "home-services-carpentry"
15
+ electrician:
16
+ label: "Electrical services"
17
+ template: "home-services-electrician"
18
+ software-contracting:
19
+ label: "Software contracting"
20
+ template: "software-contracting"
21
+ portfolio:
22
+ label: "Portfolio"
23
+ template: "portfolio"
@@ -0,0 +1,454 @@
1
+ require "fileutils"
2
+ require "pathname"
3
+ require "rails/generators"
4
+ require "rails_site_engine/profile_defaults"
5
+ require "rails_site_engine/site_profiles"
6
+
7
+ module RailsSiteEngine
8
+ module Generators
9
+ class InstallGenerator < Rails::Generators::Base
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ desc "Installs Rails Site Engine host-app defaults."
13
+
14
+ class_option :site_profile,
15
+ type: :string,
16
+ default: "home-services-general",
17
+ desc: "Concrete site profile key to use as engine defaults."
18
+ class_option :business_name,
19
+ type: :string,
20
+ desc: "Business/brand name used in profile templates."
21
+
22
+ REQUIRED_HOST_GEMS = %w[importmap-rails stimulus-rails].freeze
23
+
24
+ POSTGRES_ENV_DEFAULTS = {
25
+ "host" => '<%= ENV.fetch("POSTGRES_HOST", "127.0.0.1") %>',
26
+ "port" => '<%= ENV.fetch("POSTGRES_PORT", 54325) %>',
27
+ "username" => '<%= ENV.fetch("POSTGRES_USER", "postgres") %>',
28
+ "password" => '<%= ENV.fetch("POSTGRES_PASSWORD", "postgres") %>'
29
+ }.freeze
30
+
31
+ def ensure_required_javascript_gems
32
+ gemfile_path = host_path("Gemfile")
33
+ return unless gemfile_path.file?
34
+
35
+ content = gemfile_path.read
36
+ additions = REQUIRED_HOST_GEMS.filter_map do |gem_name|
37
+ pattern = /^\s*gem ["']#{Regexp.escape(gem_name)}["']/
38
+ next if content.match?(pattern)
39
+
40
+ %(gem "#{gem_name}")
41
+ end
42
+ return if additions.empty?
43
+
44
+ append_to_file "Gemfile", "\n#{additions.join("\n")}\n"
45
+ say_status :warning, "Added #{additions.join(', ')} to Gemfile. Run bundle install."
46
+ end
47
+
48
+ def ensure_action_mailer_railtie
49
+ application_path = host_path("config/application.rb")
50
+ return unless application_path.file?
51
+
52
+ content = application_path.read
53
+ updated = with_action_mailer_railtie(content)
54
+ return if updated == content
55
+
56
+ write_if_changed("config/application.rb", updated)
57
+ end
58
+
59
+ def ensure_development_mail_preview_gem
60
+ gemfile_path = host_path("Gemfile")
61
+ return unless gemfile_path.file?
62
+
63
+ content = gemfile_path.read
64
+ updated = with_letter_opener_web_gem(content)
65
+ return if updated == content
66
+
67
+ write_if_changed("Gemfile", updated)
68
+ end
69
+
70
+ def configure_development_action_mailer
71
+ development_path = host_path("config/environments/development.rb")
72
+ return unless development_path.file?
73
+
74
+ content = development_path.read
75
+ updated = with_development_action_mailer_defaults(content)
76
+ return if updated == content
77
+
78
+ write_if_changed("config/environments/development.rb", updated)
79
+ end
80
+
81
+ def mount_letter_opener_web
82
+ routes_path = host_path("config/routes.rb")
83
+ return unless routes_path.file?
84
+
85
+ content = routes_path.read
86
+ updated = with_letter_opener_web_route(content)
87
+ return if updated == content
88
+
89
+ write_if_changed("config/routes.rb", updated)
90
+ end
91
+
92
+ def mount_engine
93
+ routes_path = host_path("config/routes.rb")
94
+ return unless routes_path.file?
95
+
96
+ content = routes_path.read
97
+ updated = with_engine_route(content)
98
+ return if updated == content
99
+
100
+ write_if_changed("config/routes.rb", updated)
101
+ end
102
+
103
+ def create_site_engine_config
104
+ destination = host_path("config/site_engine.yml")
105
+ return if destination.file?
106
+
107
+ write_if_changed("config/site_engine.yml", site_engine_config_content)
108
+ end
109
+
110
+ def create_override_markers
111
+ scaffold_profile_site_config
112
+ scaffold_profile_theme_config
113
+ empty_directory "content/pages"
114
+ scaffold_profile_pages
115
+
116
+ overrides_doc_path = host_path("content/OVERRIDES.md")
117
+ return if overrides_doc_path.file?
118
+
119
+ write_if_changed("content/OVERRIDES.md", overrides_markdown)
120
+ end
121
+
122
+ def configure_bin_dev
123
+ write_if_changed(
124
+ "bin/dev",
125
+ <<~SH,
126
+ #!/usr/bin/env bash
127
+ set -euo pipefail
128
+
129
+ cd "$(dirname "$0")/.."
130
+
131
+ export POSTGRES_HOST="${POSTGRES_HOST:-127.0.0.1}"
132
+ export POSTGRES_PORT="${POSTGRES_PORT:-54325}"
133
+ export POSTGRES_USER="${POSTGRES_USER:-postgres}"
134
+ export POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-postgres}"
135
+
136
+ exec bin/rails server "$@"
137
+ SH
138
+ mode: 0o755
139
+ )
140
+ end
141
+
142
+ def configure_database_defaults
143
+ database_path = host_path("config/database.yml")
144
+ return unless database_path.file?
145
+
146
+ lines = database_path.read.lines
147
+ default_start = lines.index { |line| line.match?(/^default:\s*&default/) }
148
+ return if default_start.nil?
149
+
150
+ default_end =
151
+ ((default_start + 1)...lines.length).find { |index| lines[index].match?(/^[a-zA-Z0-9_]+:/) } || lines.length
152
+ section = lines[default_start...default_end]
153
+
154
+ section.reject! { |line| line.match?(/^\s*(host|port|username|password):/) }
155
+
156
+ insert_after_pool = section.index { |line| line.match?(/^\s*pool:/) || line.match?(/^\s*max_connections:/) }
157
+ insertion_index = insert_after_pool ? insert_after_pool + 1 : section.length
158
+
159
+ env_lines = POSTGRES_ENV_DEFAULTS.map { |key, value| " #{key}: #{value}\n" }
160
+ section.insert(insertion_index, *env_lines)
161
+
162
+ updated = lines[0...default_start] + section + lines[default_end..]
163
+ write_if_changed("config/database.yml", updated.join)
164
+ end
165
+
166
+ private
167
+
168
+ def site_profiles
169
+ @site_profiles ||= RailsSiteEngine::SiteProfiles.new(
170
+ path: RailsSiteEngine::Engine.root.join("config/site_profiles.yml")
171
+ )
172
+ end
173
+
174
+ def resolved_site_profile
175
+ @resolved_site_profile ||= begin
176
+ requested = options.fetch(:site_profile, "").to_s.strip
177
+ requested = "home-services-general" if requested.empty?
178
+
179
+ valid_profiles = site_profiles.concrete_profile_keys
180
+ return requested if valid_profiles.include?(requested)
181
+
182
+ raise Thor::Error,
183
+ "Unknown --site-profile=#{requested.inspect}. Valid values: #{valid_profiles.join(', ')}"
184
+ end
185
+ end
186
+
187
+ def business_name
188
+ @business_name ||= begin
189
+ configured = options.fetch(:business_name, "").to_s.strip
190
+ return configured unless configured.empty?
191
+
192
+ Pathname.new(destination_root).basename.to_s
193
+ .tr("_-", " ")
194
+ .split
195
+ .map(&:capitalize)
196
+ .join(" ")
197
+ end
198
+ end
199
+
200
+ def site_engine_config_content
201
+ <<~YAML
202
+ # Rails Site Engine host contract.
203
+ # Profile and business name are used for scaffolding client-owned content files.
204
+ profile: "#{resolved_site_profile}"
205
+ business_name: "#{business_name}"
206
+
207
+ # Content paths.
208
+ # The files at these paths are client-owned runtime content.
209
+ overrides:
210
+ site_config_path: "content/site.yml"
211
+ pages_path: "content/pages"
212
+ theme_path: "config/theme.yml"
213
+
214
+ # Example site config (content/site.yml):
215
+ # site:
216
+ # name: "Your Business"
217
+ # seo:
218
+ # default_description: "Update your marketing description"
219
+
220
+ # Example client page (content/pages/home.md):
221
+ # ---
222
+ # title: "Home"
223
+ # ---
224
+ # Custom markdown body.
225
+
226
+ # Example theme config (config/theme.yml):
227
+ # daisyui:
228
+ # theme: "corporate"
229
+ # dark_theme: "business"
230
+ YAML
231
+ end
232
+
233
+ def overrides_markdown
234
+ <<~MARKDOWN
235
+ # Content Contract
236
+
237
+ Content files are client-owned:
238
+ - `content/site.yml`
239
+ - `content/pages/*.md`
240
+ - `config/theme.yml`
241
+
242
+ The installer seeds these from the selected profile as a starting point.
243
+
244
+ Engine still provides runtime rendering and section components.
245
+
246
+ ## Files You Own
247
+
248
+ - `content/pages/<slug>.md` (required runtime content)
249
+ - `content/site.yml` (required runtime content)
250
+ - `config/theme.yml` (required runtime content)
251
+
252
+ ## Quick Example: Edit Home Page
253
+
254
+ Edit `content/pages/home.md`:
255
+
256
+ ```md
257
+ ---
258
+ title: "Home"
259
+ description: "Custom homepage description"
260
+ path: "/"
261
+ template: "landing"
262
+ show_title: false
263
+ sections:
264
+ - type: hero
265
+ heading: "Your custom headline"
266
+ subheading: "Your custom subheading"
267
+ ---
268
+ ```
269
+ MARKDOWN
270
+ end
271
+
272
+ def scaffold_profile_pages
273
+ defaults = RailsSiteEngine::ProfileDefaults.new(
274
+ profile: resolved_site_profile,
275
+ business_name: business_name,
276
+ site_profiles: site_profiles
277
+ )
278
+
279
+ defaults.page_ids.each do |page_id|
280
+ destination_path = host_path("content/pages/#{page_id}.md")
281
+ next if destination_path.file?
282
+
283
+ content = defaults.page_content(page_id).to_s
284
+ next if content.strip.empty?
285
+
286
+ write_if_changed("content/pages/#{page_id}.md", content)
287
+ end
288
+ end
289
+
290
+ def scaffold_profile_site_config
291
+ defaults = RailsSiteEngine::ProfileDefaults.new(
292
+ profile: resolved_site_profile,
293
+ business_name: business_name,
294
+ site_profiles: site_profiles
295
+ )
296
+ return if host_path("content/site.yml").file?
297
+
298
+ payload = defaults.site_config
299
+ return unless payload.is_a?(Hash) && payload.present?
300
+
301
+ write_if_changed("content/site.yml", payload.to_yaml)
302
+ end
303
+
304
+ def scaffold_profile_theme_config
305
+ defaults = RailsSiteEngine::ProfileDefaults.new(
306
+ profile: resolved_site_profile,
307
+ business_name: business_name,
308
+ site_profiles: site_profiles
309
+ )
310
+ return if host_path("config/theme.yml").file?
311
+
312
+ payload = defaults.theme_config
313
+ return unless payload.is_a?(Hash) && payload.present?
314
+
315
+ write_if_changed("config/theme.yml", payload.to_yaml)
316
+ end
317
+
318
+ def host_path(relative_path)
319
+ Pathname.new(destination_root).join(relative_path)
320
+ end
321
+
322
+ def with_action_mailer_railtie(content)
323
+ return content if content.match?(/^\s*require\s+["']rails\/all["']/)
324
+ return content if content.match?(/^\s*require\s+["']action_mailer\/railtie["']/)
325
+
326
+ uncommented = content.sub(
327
+ /^(\s*)#\s*require\s+["']action_mailer\/railtie["']\s*$/,
328
+ '\\1require "action_mailer/railtie"'
329
+ )
330
+ return uncommented unless uncommented == content
331
+
332
+ lines = content.lines
333
+ insert_after = lines.rindex do |line|
334
+ line.match?(/^\s*(?:#\s*)?require\s+["'][a-z_]+\/(?:railtie|engine)["']/)
335
+ end
336
+ if insert_after
337
+ lines.insert(insert_after + 1, %(require "action_mailer/railtie"\n))
338
+ return lines.join
339
+ end
340
+
341
+ rails_require = lines.rindex { |line| line.match?(/^\s*require\s+["']rails["']\s*$/) }
342
+ return content if rails_require.nil?
343
+
344
+ lines.insert(rails_require + 1, %(require "action_mailer/railtie"\n))
345
+ lines.join
346
+ end
347
+
348
+ def with_letter_opener_web_gem(content)
349
+ return content if content.match?(/^\s*gem ["']letter_opener_web["']/)
350
+
351
+ lines = content.lines
352
+ development_group_start = lines.index { |line| line.match?(/^\s*group\s+:development\b.*\bdo\s*$/) }
353
+
354
+ if development_group_start
355
+ development_group_end =
356
+ ((development_group_start + 1)...lines.length).find { |index| lines[index].match?(/^\s*end\s*$/) }
357
+ return content if development_group_end.nil?
358
+
359
+ indentation = lines[development_group_start][/^\s*/].to_s
360
+ lines.insert(development_group_end, %(#{indentation} gem "letter_opener_web"\n))
361
+ return lines.join
362
+ end
363
+
364
+ buffer = content.dup
365
+ buffer = "#{buffer}\n" unless buffer.end_with?("\n")
366
+ buffer << "\n#{development_mail_preview_gem_group}"
367
+ end
368
+
369
+ def with_development_action_mailer_defaults(content)
370
+ line_patterns = {
371
+ raise_delivery_errors: /^\s*config\.action_mailer\.raise_delivery_errors\s*=/,
372
+ delivery_method: /^\s*config\.action_mailer\.delivery_method\s*=/,
373
+ perform_deliveries: /^\s*config\.action_mailer\.perform_deliveries\s*=/,
374
+ default_url_options: /^\s*config\.action_mailer\.default_url_options\s*=/
375
+ }
376
+
377
+ additions = []
378
+ additions << "config.action_mailer.raise_delivery_errors = false" unless content.match?(line_patterns[:raise_delivery_errors])
379
+ additions << "config.action_mailer.delivery_method = :letter_opener_web" unless content.match?(line_patterns[:delivery_method])
380
+ additions << "config.action_mailer.perform_deliveries = true" unless content.match?(line_patterns[:perform_deliveries])
381
+ additions << 'config.action_mailer.default_url_options = { host: "localhost", port: 3000 }' unless content.match?(line_patterns[:default_url_options])
382
+ return content if additions.empty?
383
+
384
+ lines = content.lines
385
+ block_end = lines.rindex { |line| line.match?(/^\s*end\s*$/) }
386
+ return content if block_end.nil?
387
+
388
+ insertion = [ "\n" ]
389
+ insertion << " # Don't care if the mailer can't send.\n" unless content.include?("Don't care if the mailer can't send.")
390
+ insertion.concat(additions.map { |line| " #{line}\n" })
391
+
392
+ lines.insert(block_end, *insertion)
393
+ lines.join
394
+ end
395
+
396
+ def with_letter_opener_web_route(content)
397
+ lines = content.lines
398
+ draw_start = lines.index { |line| line.match?(/^\s*Rails\.application\.routes\.draw\s+do\s*$/) }
399
+ return content if draw_start.nil?
400
+
401
+ has_letter_opener_route = content.match?(/mount\s+LetterOpenerWeb::Engine,\s+at:\s+["']\/letter_opener["']/)
402
+ unless has_letter_opener_route
403
+ lines.insert(
404
+ draw_start + 1,
405
+ " if Rails.env.development?\n",
406
+ " mount LetterOpenerWeb::Engine, at: \"/letter_opener\"\n",
407
+ " end\n",
408
+ "\n"
409
+ )
410
+ end
411
+
412
+ with_engine_route(lines.join)
413
+ end
414
+
415
+ def with_engine_route(content)
416
+ lines = content.lines
417
+ draw_start = lines.index { |line| line.match?(/^\s*Rails\.application\.routes\.draw\s+do\s*$/) }
418
+ return content if draw_start.nil?
419
+
420
+ lines.reject! { |line| line.match?(/^\s*mount\s+RailsSiteEngine::Engine\s+=>\s+"\/"\s*$/) }
421
+
422
+ insert_index = draw_start + 1
423
+ if (development_if_index = lines.index { |line| line.match?(/^\s*if\s+Rails\.env\.development\?\s*$/) })
424
+ development_end_index = ((development_if_index + 1)...lines.length).find { |index| lines[index].match?(/^\s*end\s*$/) }
425
+ insert_index = development_end_index ? (development_end_index + 1) : insert_index
426
+ insert_index += 1 if lines[insert_index]&.strip == ""
427
+ end
428
+
429
+ lines.insert(insert_index, " mount RailsSiteEngine::Engine => \"/\"\n")
430
+ lines.insert(insert_index + 1, "\n") unless lines[insert_index + 1]&.strip == ""
431
+
432
+ lines.join
433
+ end
434
+
435
+ def development_mail_preview_gem_group
436
+ <<~RUBY
437
+ group :development do
438
+ gem "letter_opener_web"
439
+ end
440
+ RUBY
441
+ end
442
+
443
+ def write_if_changed(relative_path, content, mode: nil)
444
+ path = host_path(relative_path)
445
+ existing = path.file? ? path.read : nil
446
+ return if existing == content
447
+
448
+ FileUtils.mkdir_p(path.dirname)
449
+ File.write(path, content)
450
+ File.chmod(mode, path) if mode
451
+ end
452
+ end
453
+ end
454
+ end
@@ -0,0 +1,3 @@
1
+ daisyui:
2
+ theme: "corporate"
3
+ dark_theme: "business"
@@ -0,0 +1,12 @@
1
+ ---
2
+ title: "About"
3
+ description: "A short about page description."
4
+ path: "/about"
5
+ template: "standard"
6
+ show_title: true
7
+ lead: "A quick introduction to your team and values."
8
+ sections:
9
+ - type: rich_text
10
+ ---
11
+
12
+ Write a short story about the business.
@@ -0,0 +1,13 @@
1
+ ---
2
+ title: "Contact"
3
+ description: "Get in touch."
4
+ path: "/contact"
5
+ template: "contact"
6
+ show_title: true
7
+ lead: "Tell us what you need and we will get back to you quickly."
8
+ sections:
9
+ - type: contact_form
10
+ heading: "Request a quote"
11
+ body: "Please share your project details and preferred timeline."
12
+ ---
13
+