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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +137 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/rails_site_engine/application.css +15 -0
- data/app/assets/stylesheets/rails_site_engine/engine.css +2 -0
- data/app/assets/stylesheets/rails_site_engine/engine.tailwind.css +5 -0
- data/app/controllers/rails_site_engine/application_controller.rb +27 -0
- data/app/controllers/rails_site_engine/contacts_controller.rb +35 -0
- data/app/controllers/rails_site_engine/meta_controller.rb +20 -0
- data/app/controllers/rails_site_engine/pages_controller.rb +18 -0
- data/app/helpers/rails_site_engine/application_helper.rb +266 -0
- data/app/javascript/controllers/rails_site_engine/color_mode_controller.js +83 -0
- data/app/javascript/controllers/rails_site_engine/form_submit_controller.js +22 -0
- data/app/javascript/controllers/rails_site_engine/mobile_nav_controller.js +45 -0
- data/app/javascript/rails_site_engine/application.js +8 -0
- data/app/jobs/rails_site_engine/application_job.rb +4 -0
- data/app/mailers/rails_site_engine/application_mailer.rb +6 -0
- data/app/mailers/rails_site_engine/contact_mailer.rb +25 -0
- data/app/models/rails_site_engine/application_record.rb +5 -0
- data/app/models/rails_site_engine/contact_message.rb +16 -0
- data/app/views/layouts/mailer.html.erb +13 -0
- data/app/views/layouts/mailer.text.erb +1 -0
- data/app/views/layouts/rails_site_engine/application.html.erb +54 -0
- data/app/views/rails_site_engine/contact_mailer/notify.text.erb +9 -0
- data/app/views/rails_site_engine/contacts/new.html.erb +1 -0
- data/app/views/rails_site_engine/pages/show.html.erb +15 -0
- data/app/views/rails_site_engine/sections/_contact_form.html.erb +112 -0
- data/app/views/rails_site_engine/sections/_cta_band.html.erb +27 -0
- data/app/views/rails_site_engine/sections/_faq.html.erb +29 -0
- data/app/views/rails_site_engine/sections/_feature_cards.html.erb +30 -0
- data/app/views/rails_site_engine/sections/_hero.html.erb +39 -0
- data/app/views/rails_site_engine/sections/_pricing_cards.html.erb +53 -0
- data/app/views/rails_site_engine/sections/_process_steps.html.erb +35 -0
- data/app/views/rails_site_engine/sections/_project_cards.html.erb +48 -0
- data/app/views/rails_site_engine/sections/_proof_strip.html.erb +16 -0
- data/app/views/rails_site_engine/sections/_rich_text.html.erb +15 -0
- data/app/views/rails_site_engine/sections/_stats.html.erb +34 -0
- data/app/views/rails_site_engine/sections/_testimonials.html.erb +40 -0
- data/app/views/rails_site_engine/sections/_unknown.html.erb +6 -0
- data/app/views/rails_site_engine/shared/_flash.html.erb +18 -0
- data/app/views/rails_site_engine/shared/_footer.html.erb +41 -0
- data/app/views/rails_site_engine/shared/_navbar.html.erb +118 -0
- data/config/importmap.rb +5 -0
- data/config/locales/en.yml +27 -0
- data/config/routes.rb +9 -0
- data/config/site_profiles.yml +23 -0
- data/lib/generators/rails_site_engine/install/install_generator.rb +454 -0
- data/lib/generators/rails_site_engine/install/templates/config/theme.yml +3 -0
- data/lib/generators/rails_site_engine/install/templates/content/pages/about.md +12 -0
- data/lib/generators/rails_site_engine/install/templates/content/pages/contact.md +13 -0
- data/lib/generators/rails_site_engine/install/templates/content/pages/home.md +39 -0
- data/lib/generators/rails_site_engine/install/templates/content/pages/privacy.md +43 -0
- data/lib/generators/rails_site_engine/install/templates/content/pages/services.md +25 -0
- data/lib/generators/rails_site_engine/install/templates/content/site.yml +36 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/config/theme.yml +3 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/about.md +14 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/contact.md +12 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/home.md +55 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/privacy.md +43 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/services.md +29 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/site.yml +36 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/config/theme.yml +3 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/about.md +14 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/contact.md +12 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/home.md +55 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/privacy.md +43 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/services.md +29 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/site.yml +36 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/config/theme.yml +3 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/about.md +14 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/contact.md +12 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/home.md +55 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/privacy.md +43 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/services.md +29 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/site.yml +36 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/config/theme.yml +3 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/about.md +14 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/contact.md +12 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/home.md +55 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/privacy.md +43 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/services.md +29 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/site.yml +36 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/config/theme.yml +3 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/about.md +26 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/contact.md +12 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/home.md +43 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/privacy.md +43 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/work.md +27 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/site.yml +36 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/config/theme.yml +3 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/about.md +14 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/contact.md +12 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/home.md +48 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/pricing.md +57 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/privacy.md +43 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/projects.md +27 -0
- data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/site.yml +38 -0
- data/lib/rails_site_engine/content/store.rb +297 -0
- data/lib/rails_site_engine/engine.rb +15 -0
- data/lib/rails_site_engine/markdown_renderer.rb +37 -0
- data/lib/rails_site_engine/profile_defaults.rb +83 -0
- data/lib/rails_site_engine/site_engine_config.rb +78 -0
- data/lib/rails_site_engine/site_profiles.rb +166 -0
- data/lib/rails_site_engine/sitemap_xml.rb +26 -0
- data/lib/rails_site_engine/theme.rb +54 -0
- data/lib/rails_site_engine/version.rb +3 -0
- data/lib/rails_site_engine.rb +13 -0
- data/lib/tasks/rails_site_engine_tasks.rake +7 -0
- 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>
|
data/config/importmap.rb
ADDED
|
@@ -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,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
|
+
|