Class: StaticSiteBuilder::Builder
- Inherits:
-
Object
- Object
- StaticSiteBuilder::Builder
- Defined in:
- lib/static_site_builder/builder.rb
Constant Summary collapse
- DIST_DIR =
Output directory name
"dist"- JS_ENTRY_POINT =
Default JavaScript entry point
"application"
Instance Method Summary collapse
- #annotate_template(content, template_path) ⇒ Object
-
#build ⇒ void
Builds the complete static site.
- #compile_erb_page(erb_file, page_name, dist_dir) ⇒ Object
- #compile_erb_pages(dist_dir) ⇒ Object
- #copy_assets(dist_dir) ⇒ Object
- #copy_static_files(dist_dir) ⇒ Object
- #default_layout ⇒ Object
-
#hash_value(hash, *keys) ⇒ Object?
Accesses hash values using either symbol or string keys.
-
#initialize(root: Dir.pwd, annotate_template_file_names: nil) ⇒ Builder
constructor
A new instance of Builder.
-
#live_reload_script ⇒ Object
Generate live reload WebSocket script if live reload is enabled.
-
#load_helpers(view_class) ⇒ Object
Automatically load helper modules from app/helpers/ directory.
-
#load_layout_content ⇒ Object
Load layout content, trying .html.erb first, then .html, or default layout.
-
#render_layout_template(view, layout_content, layout_file, page_content) ⇒ Object
Render layout template using ActionView with proper yield mechanism.
-
#render_page_template(view, content, page_name, erb_file) ⇒ Object
Render page template using ActionView (file-based, not inline).
-
#setup_action_view_context ⇒ Object
Setup ActionView context and create view instance.
-
#write_page_output(dist_dir, page_name, rendered) ⇒ Object
Write final rendered output to file.
Constructor Details
#initialize(root: Dir.pwd, annotate_template_file_names: nil) ⇒ Builder
Returns a new instance of Builder.
51 52 53 54 55 56 57 58 59 60 |
# File 'lib/static_site_builder/builder.rb', line 51 def initialize(root: Dir.pwd, annotate_template_file_names: nil) @root = Pathname.new(root) # Auto-enable annotations in development (when LIVE_RELOAD is enabled) @annotate_template_file_names = if annotate_template_file_names.nil? ENV["LIVE_RELOAD"] == "true" || ENV["RAILS_ENV"] == "development" else annotate_template_file_names end end |
Instance Method Details
#annotate_template(content, template_path) ⇒ Object
382 383 384 385 386 387 388 389 390 391 392 |
# File 'lib/static_site_builder/builder.rb', line 382 def annotate_template(content, template_path) # Remove any existing annotations to avoid duplicates content = content.gsub(/<!-- BEGIN .*? -->\n?/, "") content = content.gsub(/\n?<!-- END .*? -->/, "") begin_comment = "<!-- BEGIN #{template_path} -->" end_comment = "<!-- END #{template_path} -->" # Wrap content with template path annotations "#{begin_comment}\n#{content}\n#{end_comment}" end |
#build ⇒ void
This method returns an undefined value.
Builds the complete static site.
Compiles ERB templates to HTML, copies JavaScript and CSS assets, and outputs everything to the dist/ directory. In development mode, files are updated in place to prevent 404 errors during live reload. In production mode, the dist directory is cleaned first for a fresh build.
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/static_site_builder/builder.rb', line 70 def build puts "Building static site..." dist_dir = @root.join(DIST_DIR) # Clean dist directory only for production/release builds # In development, update files in place to prevent 404s during live reload production_build = ENV["PRODUCTION"] == "true" || ENV["RELEASE"] == "true" if production_build if dist_dir.exist? puts "Cleaning dist directory for production build..." FileUtils.rm_rf(dist_dir) end end # Ensure dist directory exists FileUtils.mkdir_p(dist_dir) # Copy JavaScript and CSS assets to dist copy_assets(dist_dir) # Compile ERB templates to static HTML pages compile_erb_pages(dist_dir) # Copy static files from public/ directory to dist copy_static_files(dist_dir) # Notify WebSocket server of rebuild for live reload # Always update the reload file, even if it doesn't exist yet reload_file = @root.join(".reload") File.write(reload_file, Time.current.to_f.to_s) puts "\n✓ Build complete! Output in #{dist_dir}" end |
#compile_erb_page(erb_file, page_name, dist_dir) ⇒ Object
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 |
# File 'lib/static_site_builder/builder.rb', line 351 def compile_erb_page(erb_file, page_name, dist_dir) puts "Compiling page: #{page_name}..." layout_content, layout_file = load_layout_content view = setup_action_view_context # Render page template first - this sets up meta tags and content_for blocks # Pass erb_file directly - render_page_template will use file-based rendering # Page templates set their own @title, @description etc. using meta-tags gem page_content = render_page_template(view, nil, page_name, erb_file) # Now render layout with page content available via yield rendered = render_layout_template(view, layout_content, layout_file, page_content) write_page_output(dist_dir, page_name, rendered) puts " ✓ Created #{page_name}" end |
#compile_erb_pages(dist_dir) ⇒ Object
338 339 340 341 342 343 344 345 346 347 348 349 |
# File 'lib/static_site_builder/builder.rb', line 338 def compile_erb_pages(dist_dir) pages_dir = @root.join("app", "views", "pages") return unless pages_dir.exist? # Find all ERB files, including nested directories Dir.glob(pages_dir.join("**", "*.html.erb")).each do |erb_file| relative_path = Pathname.new(erb_file).relative_path_from(pages_dir) page_name = relative_path.to_s.gsub(/\.html\.erb$/, ".html") compile_erb_page(erb_file, page_name, dist_dir) end end |
#copy_assets(dist_dir) ⇒ Object
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 |
# File 'lib/static_site_builder/builder.rb', line 310 def copy_assets(dist_dir) puts "Copying assets..." # Copy JavaScript files from app/javascript to dist/assets/javascripts js_dir = @root.join("app", "javascript") if js_dir.exist? && js_dir.directory? dist_js = dist_dir.join("assets", "javascripts") FileUtils.mkdir_p(dist_js) # Copy all files and subdirectories recursively Dir.glob(js_dir.join("*")).each do |item| FileUtils.cp_r(item, dist_js, preserve: true) end end # Handle CSS files # Copy CSS files from app/assets/stylesheets to dist/assets/stylesheets css_dir = @root.join("app", "assets", "stylesheets") dist_css = dist_dir.join("assets", "stylesheets") if css_dir.exist? && css_dir.directory? FileUtils.mkdir_p(dist_css) Dir.glob(css_dir.join("*")).each do |item| FileUtils.cp_r(item, dist_css, preserve: true) end end end |
#copy_static_files(dist_dir) ⇒ Object
371 372 373 374 375 376 377 378 379 380 |
# File 'lib/static_site_builder/builder.rb', line 371 def copy_static_files(dist_dir) public_dir = @root.join("public") return unless public_dir.exist? && public_dir.directory? puts "Copying static files from public/..." # Copy all files and subdirectories from public to dist Dir.glob(public_dir.join("*")).each do |item| FileUtils.cp_r(item, dist_dir, preserve: true) end end |
#default_layout ⇒ Object
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 |
# File 'lib/static_site_builder/builder.rb', line 394 def default_layout live_reload_script_content = live_reload_script <<~HTML <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <%# Set meta tags in your page templates using: %> <%# <% set_meta_tags title: 'Page Title', description: 'Description' %> %> <%= display_meta_tags %> <link rel="stylesheet" href="/assets/stylesheets/application.css"> </head> <body> <%= yield %> <% if content_for?(:javascript) %> <%= yield(:javascript) %> <% end %> #{live_reload_script_content} </body> </html> HTML end |
#hash_value(hash, *keys) ⇒ Object?
Accesses hash values using either symbol or string keys.
Tries each key in order until a non-nil value is found. This allows compatibility with both symbol and string keys in options hashes.
118 119 120 121 122 123 124 |
# File 'lib/static_site_builder/builder.rb', line 118 def hash_value(hash, *keys) keys.each do |key| value = hash[key] return value if value.present? end nil end |
#live_reload_script ⇒ Object
Generate live reload WebSocket script if live reload is enabled
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 |
# File 'lib/static_site_builder/builder.rb', line 287 def live_reload_script if ENV["LIVE_RELOAD"] == "true" ws_port = ENV["WS_PORT"] || StaticSiteBuilder::DEFAULT_WS_PORT <<~HTML <script> (function() { function connect() { var ws = new WebSocket('ws://localhost:#{ws_port}'); ws.onmessage = function(e) { if (e.data === 'reload') window.location.reload(); }; ws.onclose = function() { setTimeout(connect, 1000); }; ws.onerror = function() {}; } connect(); })(); </script> HTML else "" end end |
#load_helpers(view_class) ⇒ Object
Automatically load helper modules from app/helpers/ directory
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/static_site_builder/builder.rb', line 166 def load_helpers(view_class) helpers_dir = @root.join('app', 'helpers') return unless helpers_dir.exist? && helpers_dir.directory? Dir.glob(helpers_dir.join('**', '*_helper.rb')).each do |helper_file| begin # Get the module name from the file (e.g., app/helpers/application_helper.rb -> ApplicationHelper) relative_path = Pathname.new(helper_file).relative_path_from(helpers_dir) module_name = relative_path.to_s.gsub(/\.rb$/, '').split('_').map(&:capitalize).join # Load the file (use load instead of require for absolute paths) load helper_file # Include the module if it exists if Object.const_defined?(module_name) helper_module = Object.const_get(module_name) view_class.include(helper_module) unless view_class.included_modules.include?(helper_module) end rescue LoadError, NameError => e # Silently skip if helper can't be loaded (e.g., missing dependencies) # This allows users to have helpers that require additional gems end end end |
#load_layout_content ⇒ Object
Load layout content, trying .html.erb first, then .html, or default layout
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/static_site_builder/builder.rb', line 128 def load_layout_content layout = StaticSiteBuilder::DEFAULT_LAYOUT_NAME layout_file = @root.join('app', 'views', 'layouts', "#{layout}.html.erb") layout_file = @root.join('app', 'views', 'layouts', "#{layout}.html") unless layout_file.exist? layout_content = layout_file.exist? ? File.read(layout_file) : default_layout # Inject live reload script if enabled and using custom layout if ENV['LIVE_RELOAD'] == 'true' && layout_file.exist? unless layout_content.include?('live reload') || layout_content.include?('LIVE_RELOAD') script = live_reload_script layout_content = layout_content.gsub(/<\/body>/, "#{script}</body>") unless script.blank? end end [layout_content, layout_file] end |
#render_layout_template(view, layout_content, layout_file, page_content) ⇒ Object
Render layout template using ActionView with proper yield mechanism
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 |
# File 'lib/static_site_builder/builder.rb', line 235 def render_layout_template(view, layout_content, layout_file, page_content) layout = StaticSiteBuilder::DEFAULT_LAYOUT_NAME # Make page content safe for HTML output safe_page_content = page_content.respond_to?(:html_safe) ? page_content.html_safe : page_content # Create layout template layout_template = ActionView::Template.new( layout_content, layout_file.exist? ? layout_file.to_s : 'inline:layout', ActionView::Template::Handlers::ERB.new, virtual_path: "layouts/#{layout}", format: :html, locals: [] ) # Render layout with page content available via yield # In Rails/ActionView, yield in a layout template returns the rendered page content # We achieve this by storing the page content in the view flow before rendering the layout # The view flow's :layout key is what yield accesses original_flow_content = view.view_flow.get(:layout) view.view_flow.set(:layout, safe_page_content) rendered = view.render(template: layout_template) # Restore original flow content if it existed if original_flow_content view.view_flow.set(:layout, original_flow_content) else view.view_flow.set(:layout, nil) end if @annotate_template_file_names && layout_file.exist? relative_layout_path = Pathname.new(layout_file).relative_path_from(@root) begin_comment = "<!-- BEGIN #{relative_layout_path} -->" end_comment = "<!-- END #{relative_layout_path} -->" rendered = "#{begin_comment}\n#{rendered}\n#{end_comment}" end rendered end |
#render_page_template(view, content, page_name, erb_file) ⇒ Object
Render page template using ActionView (file-based, not inline)
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/static_site_builder/builder.rb', line 194 def render_page_template(view, content, page_name, erb_file) # Calculate the template path relative to app/views # e.g., app/views/pages/index.html.erb -> pages/index pages_dir = @root.join("app", "views", "pages") template_path = Pathname.new(erb_file).relative_path_from(pages_dir).to_s.gsub(/\.html\.erb$/, '') full_template_path = "pages/#{template_path}" # Set prefixes on lookup_context so ActionView can resolve partials relative to template directory # For pages/index -> prefix is 'pages', for pages/blog/index -> prefix is 'pages/blog' lookup_context = view.lookup_context original_prefixes = lookup_context.prefixes.dup template_dir = File.dirname(full_template_path) lookup_context.prefixes = [template_dir] begin # Set instance variables on view (Rails pattern: controllers set instance variables) # Templates set their own @title, @description etc. using meta-tags gem # Use file-based rendering - ActionView will find the actual file # With prefixes set, partials can be resolved relative to the template directory template = lookup_context.find_template(full_template_path, [], false, [], {}) page_content = view.render(template: template) rescue ActionView::Template::Error => e if e.cause.is_a?(ActionView::MissingTemplate) raise "Partial template not found. Searched in: #{e.cause.path}" end raise ensure # Restore original prefixes lookup_context.prefixes = original_prefixes end if @annotate_template_file_names relative_template_path = Pathname.new(erb_file).relative_path_from(@root) page_content = annotate_template(page_content, relative_template_path.to_s) end page_content end |
#setup_action_view_context ⇒ Object
Setup ActionView context and create view instance
Creates an ActionView::Base instance with helpers included (Rails pattern). Helpers are included on the class before instantiation, which is more Rails-like than extending individual instances.
150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/static_site_builder/builder.rb', line 150 def setup_action_view_context view_paths = ActionView::PathSet.new([@root.join('app', 'views').to_s]) lookup_context = ActionView::LookupContext.new(view_paths) view_class = ActionView::Base.with_empty_template_cache # Include helpers on the class (Rails pattern) rather than extending instances view_class.include(MetaTags::ViewHelper) unless view_class.included_modules.include?(MetaTags::ViewHelper) # Automatically load helpers from app/helpers/ load_helpers(view_class) view = view_class.new(lookup_context, {}, self) view end |
#write_page_output(dist_dir, page_name, rendered) ⇒ Object
Write final rendered output to file
278 279 280 281 282 |
# File 'lib/static_site_builder/builder.rb', line 278 def write_page_output(dist_dir, page_name, rendered) output_path = dist_dir.join(page_name) FileUtils.mkdir_p(output_path.dirname) File.write(output_path, rendered) end |