static-site-builder 0.0.1 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34651b912c8819f93ee3dd7dda4209674306e91ff9040d8343671412ba4a9873
4
- data.tar.gz: 32ac2a4ce16874db16d7990607dbc557ab18fa0235670681377aabb0baf78684
3
+ metadata.gz: cfa2d9ecc9c90e973fd3cecd6c04e2aad09a80fa29ba0bd78a95b524206986c4
4
+ data.tar.gz: 2394ef787382dec487390d4b05a1add37a27e3e9154f032f82d9972fae25924e
5
5
  SHA512:
6
- metadata.gz: 96ace8067d05e208f9d26061962558fc9bfef30196e703be13249573612fb674ab496e6e052522e5c8ab6006b39b6825108d89a360323b5c4e6d2e9c02badc96
7
- data.tar.gz: 2656f4592bf359cd186284ebbba773e83772fda781121e9515cee27d9f72b7fa7e6520c23858464842da95c9fab197782c7a4b510ef2b7cf3cbb0b2e7207be8d
6
+ metadata.gz: 89d7320b08a5e90d12f7c155a8f2e2b0a7f907b9ff18c53c81632dcc5f49cd90e34d41b1533baf760bd4866a07aa06ad95a6f84ac4d34600ed8a86296a24b25e
7
+ data.tar.gz: 7db49008d74e0aa849e084edb8a8ccc515c1764c3d97e3fb820e952697d3187dd3723e4f4edb4dc284b1dff5ccf5549de7de282413c4616d619d7a8addb693da
data/CHANGELOG.md CHANGED
@@ -5,6 +5,45 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.3] - 2025-11-22
9
+
10
+ ### Added
11
+ - Integrated ActionView 8+ for proper Rails-style partial rendering
12
+ - Support for Rails-style render syntax: `render 'shared/header'` and `render partial: 'shared/header'`
13
+ - Support for passing locals to partials: `render partial: 'shared/header', locals: { title: 'Hello' }`
14
+ - Nested partial support (partials can render other partials)
15
+ - Multiple partials on the same page now work correctly
16
+
17
+ ### Changed
18
+ - Replaced raw ERB implementation with ActionView::Base for template rendering
19
+ - Render method now uses ActionView's rendering system, matching Rails behaviour exactly
20
+ - Partials automatically receive page variables (frontmatter, js_modules, importmap_json, current_page)
21
+ - Improved error messages for missing partials (converted from ActionView format for backwards compatibility)
22
+ - Template annotations now preserve both page and layout annotations correctly
23
+
24
+ ### Fixed
25
+ - Fixed issue where nested partials (partials rendering other partials) would fail or produce incorrect output
26
+ - Fixed issue where multiple partials on the same page would only render the last one
27
+ - Fixed template annotations being stripped when layout annotations were added
28
+
29
+ ## [0.1.2] - 2025-11-22
30
+
31
+ ### Fixed
32
+ - CSS directory is now always created when Tailwind handles CSS compilation, preventing 404 errors for stylesheets
33
+ - Build process now updates files in place instead of cleaning and recreating the dist directory, preventing 404 errors during rebuilds in development mode
34
+ - Fixed race condition where pages would return 404 when code changes triggered rebuilds
35
+
36
+ ## [0.1.1] - 2025-11-21
37
+
38
+ ### Added
39
+ - `render` helper method for ERB templates to include partials
40
+ - Support for rendering partials from `app/views/shared/` directory
41
+ - Partial files should be named with `_` prefix (e.g., `_header.html.erb`)
42
+ - Partials have access to page variables (frontmatter, js_modules, etc.)
43
+
44
+ ### Changed
45
+ - Improved ERB compilation to support partial rendering
46
+
8
47
  ## [0.0.1] - 2025-11-21
9
48
 
10
49
  ### Added
data/lib/generator.rb CHANGED
@@ -70,6 +70,7 @@ module StaticSiteBuilder
70
70
  create_build_files
71
71
  create_example_pages
72
72
  create_readme
73
+ create_gitignore
73
74
 
74
75
  puts "\nāœ“ Site generated successfully!"
75
76
  puts "\nNext steps:"
@@ -580,6 +581,18 @@ module StaticSiteBuilder
580
581
  @tailwind base;
581
582
  @tailwind components;
582
583
  @tailwind utilities;
584
+
585
+ @layer base {
586
+ html {
587
+ scroll-behavior: smooth;
588
+ }
589
+ }
590
+
591
+ @layer utilities {
592
+ section[id] {
593
+ scroll-margin-top: 5rem;
594
+ }
595
+ }
583
596
  CSS
584
597
  when "shadcn"
585
598
  write_file("app/assets/stylesheets/application.css", <<~CSS)
@@ -614,6 +627,7 @@ module StaticSiteBuilder
614
627
  require "webrick"
615
628
  require "fileutils"
616
629
  require "static_site_builder/websocket_server"
630
+ require "json"
617
631
 
618
632
  port = ENV["PORT"] || 3000
619
633
  ws_port = ENV["WS_PORT"] || 3001
@@ -629,13 +643,33 @@ module StaticSiteBuilder
629
643
  ENV["WS_PORT"] = ws_port.to_s
630
644
  Rake::Task["build:all"].invoke
631
645
 
646
+ # Check if we need to run Tailwind CSS watch (after initial build)
647
+ tailwind_pid = nil
648
+ package_json_path = Pathname.new(Dir.pwd).join("package.json")
649
+ if package_json_path.exist?
650
+ package_json = JSON.parse(File.read(package_json_path))
651
+ if package_json.dig("scripts", "watch:css")
652
+ puts "šŸŽØ Starting Tailwind CSS watch mode..."
653
+ tailwind_pid = spawn("npm", "run", "watch:css", :err => File::NULL, :out => File::NULL)
654
+ # Touch the source file to trigger Tailwind watch to process CSS immediately
655
+ css_source = Pathname.new(Dir.pwd).join("app", "assets", "stylesheets", "application.css")
656
+ if css_source.exist?
657
+ FileUtils.touch(css_source)
658
+ end
659
+ # Give Tailwind a moment to process CSS
660
+ sleep 1.5
661
+ end
662
+ end
663
+
632
664
  puts "\nšŸš€ Starting development server at http://localhost:#{port}"
633
665
  puts "šŸ“” WebSocket server at ws://localhost:#{ws_port}"
634
666
  puts "šŸ“ Watching for changes... (Ctrl+C to stop)"
635
667
  puts "šŸ”„ Live reload enabled - pages will auto-refresh on changes\n"
636
668
 
637
- # Simple file watcher - just rebuild when files change (no relaunch needed)
638
- watcher_code = %q{watched = ['app', 'config']; exts = ['.erb', '.rb', '.js', '.css']; mtimes = {}; loop do; changed = false; watched.each do |dir|; Dir.glob(File.join(dir, '**', '*')).each do |f|; next unless File.file?(f) && exts.any? { |e| f.end_with?(e) }; mtime = File.mtime(f); if mtimes[f] != mtime; mtimes[f] = mtime; changed = true; end; end; end; system('rake build:all > /dev/null 2>&1') if changed; sleep 0.5; end}
669
+ # Simple file watcher - rebuild HTML when non-CSS files change
670
+ # CSS changes are handled by Tailwind watch, so we skip rebuild for CSS files
671
+ # When HTML rebuilds, it cleans dist, so we need to rebuild CSS immediately after
672
+ watcher_code = %q{watched = ['app', 'config']; exts = ['.erb', '.rb', '.js']; mtimes = {}; loop do; changed = false; watched.each do |dir|; Dir.glob(File.join(dir, '**', '*')).each do |f|; next unless File.file?(f) && exts.any? { |e| f.end_with?(e) }; next if f.end_with?('.css'); mtime = File.mtime(f); if mtimes[f] != mtime; mtimes[f] = mtime; changed = true; end; end; end; if changed; system('rake build:html > /dev/null 2>&1 && rake build:css > /dev/null 2>&1'); end; sleep 0.5; end}
639
673
  watcher_pid = spawn("ruby", "-e", watcher_code, :err => File::NULL)
640
674
 
641
675
  # Start web server
@@ -648,6 +682,7 @@ module StaticSiteBuilder
648
682
  trap("INT") do
649
683
  puts "\n\nShutting down..."
650
684
  Process.kill("TERM", watcher_pid) if watcher_pid
685
+ Process.kill("TERM", tailwind_pid) if tailwind_pid
651
686
  ws_server.stop
652
687
  server.shutdown
653
688
  end
@@ -693,27 +728,56 @@ module StaticSiteBuilder
693
728
  require "pathname"
694
729
 
695
730
  namespace :build do
696
- desc "Build everything (assets + HTML)"
697
- task :all => [:assets, :html] do
731
+ desc "Build everything (HTML + CSS)"
732
+ task :all => [:html, :css] do
698
733
  puts "\\nāœ“ Build complete!"
699
734
  end
700
735
 
701
- desc "Build JavaScript/CSS assets"
736
+ desc "Build JavaScript assets"
702
737
  task :assets do
703
- sh "npm run build" if File.exist?("package.json")
738
+ if File.exist?("package.json")
739
+ package_json = JSON.parse(File.read("package.json"))
740
+ build_script = package_json.dig("scripts", "build")
741
+ # Only run if build script exists and doesn't include CSS (CSS handled separately)
742
+ if build_script && !build_script.include?("build:css")
743
+ sh "npm run build"
744
+ end
745
+ end
704
746
  end
705
747
 
706
748
  desc "Compile all pages to static HTML"
707
- task :html do
749
+ task :html => [:assets] do
708
750
  load "lib/site_builder.rb"
709
751
  end
710
752
 
753
+ desc "Build CSS (runs after HTML so dist directory exists)"
754
+ task :css do
755
+ if File.exist?("package.json")
756
+ package_json = JSON.parse(File.read("package.json"))
757
+ if package_json.dig("scripts", "build:css")
758
+ sh "npm run build:css"
759
+ end
760
+ elsif File.exist?("tailwind.config.js")
761
+ # Build CSS even if no package.json (standalone Tailwind)
762
+ if system("which tailwindcss > /dev/null 2>&1")
763
+ FileUtils.mkdir_p("dist/assets/stylesheets")
764
+ sh "tailwindcss -i ./app/assets/stylesheets/application.css -o ./dist/assets/stylesheets/application.css --minify"
765
+ end
766
+ end
767
+ end
768
+
711
769
  desc "Clean dist directory"
712
770
  task :clean do
713
771
  dist_dir = Pathname.new(Dir.pwd).join("dist")
714
772
  FileUtils.rm_rf(dist_dir) if dist_dir.exist?
715
773
  puts "Cleaned \#{dist_dir}"
716
774
  end
775
+
776
+ desc "Build for production/release (cleans dist directory first)"
777
+ task :production do
778
+ ENV["PRODUCTION"] = "true"
779
+ Rake::Task["build:all"].invoke
780
+ end
717
781
  end
718
782
 
719
783
  namespace :dev do
@@ -750,6 +814,12 @@ module StaticSiteBuilder
750
814
  FileUtils.rm_rf(dist_dir) if dist_dir.exist?
751
815
  puts "Cleaned \#{dist_dir}"
752
816
  end
817
+
818
+ desc "Build for production/release (cleans dist directory first)"
819
+ task :production do
820
+ ENV["PRODUCTION"] = "true"
821
+ Rake::Task["build:all"].invoke
822
+ end
753
823
  end
754
824
 
755
825
  namespace :dev do
@@ -887,7 +957,7 @@ module StaticSiteBuilder
887
957
  end
888
958
 
889
959
  def build_script
890
- case @options[:js_bundler]
960
+ js_build = case @options[:js_bundler]
891
961
  when "esbuild"
892
962
  "node esbuild.config.js"
893
963
  when "webpack"
@@ -895,7 +965,20 @@ module StaticSiteBuilder
895
965
  when "vite"
896
966
  "vite build"
897
967
  else
898
- "echo 'No JS bundling needed'"
968
+ nil
969
+ end
970
+
971
+ css_build = if needs_css_build?
972
+ "npm run build:css"
973
+ else
974
+ nil
975
+ end
976
+
977
+ builds = [js_build, css_build].compact
978
+ if builds.empty?
979
+ "echo 'No bundling needed'"
980
+ else
981
+ builds.join(" && ")
899
982
  end
900
983
  end
901
984
 
@@ -924,6 +1007,41 @@ module StaticSiteBuilder
924
1007
  @options[:css_framework] == "tailwindcss" || @options[:css_framework] == "shadcn"
925
1008
  end
926
1009
 
1010
+ def create_gitignore
1011
+ content = <<~GITIGNORE
1012
+ # Dependencies
1013
+ /.bundle/
1014
+ /vendor/bundle
1015
+ /node_modules/
1016
+
1017
+ # Build artifacts
1018
+ *.gem
1019
+ *.gemspec.bak
1020
+ /dist/
1021
+ /tmp/
1022
+ /coverage/
1023
+
1024
+ # Test artifacts
1025
+ /.rspec_status
1026
+
1027
+ # IDE
1028
+ /.idea/
1029
+ /.vscode/
1030
+ *.swp
1031
+ *.swo
1032
+ *~
1033
+
1034
+ # OS
1035
+ .DS_Store
1036
+ Thumbs.db
1037
+
1038
+ # Logs
1039
+ *.log
1040
+ GITIGNORE
1041
+
1042
+ write_file(".gitignore", content)
1043
+ end
1044
+
927
1045
  def write_file(path, content)
928
1046
  file_path = @app_path.join(path)
929
1047
  FileUtils.mkdir_p(file_path.dirname)
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "uri"
4
+ require "action_view"
5
+ require "action_view/helpers"
3
6
  require "erb"
4
7
  require "fileutils"
5
8
  require "json"
@@ -49,18 +52,30 @@ module StaticSiteBuilder
49
52
  def build
50
53
  puts "Building static site..."
51
54
 
52
- # Clean dist directory
53
55
  dist_dir = @root.join("dist")
54
- FileUtils.rm_rf(dist_dir) if dist_dir.exist?
56
+
57
+ # Only clean dist directory for production/release builds
58
+ # In development, update files in place to prevent 404s during rebuilds
59
+ production_build = ENV["PRODUCTION"] == "true" || ENV["RELEASE"] == "true"
60
+ if production_build
61
+ if dist_dir.exist?
62
+ puts "Cleaning dist directory for production build..."
63
+ FileUtils.rm_rf(dist_dir)
64
+ else
65
+ puts "Dist directory does not exist, skipping clean"
66
+ end
67
+ end
68
+
69
+ # Ensure dist directory exists
55
70
  FileUtils.mkdir_p(dist_dir)
56
71
 
57
- # Copy assets
72
+ # Copy assets (overwrites existing files)
58
73
  copy_assets(dist_dir)
59
74
 
60
- # Generate importmap JSON if using importmap
75
+ # Generate importmap JSON if using importmap (overwrites existing file)
61
76
  generate_importmap(dist_dir) if @js_bundler == "importmap"
62
77
 
63
- # Compile pages based on template engine
78
+ # Compile pages based on template engine (overwrites existing files)
64
79
  case @template_engine
65
80
  when "erb"
66
81
  compile_erb_pages(dist_dir)
@@ -68,7 +83,7 @@ module StaticSiteBuilder
68
83
  compile_phlex_pages(dist_dir)
69
84
  end
70
85
 
71
- # Copy static files
86
+ # Copy static files (overwrites existing files)
72
87
  copy_static_files(dist_dir)
73
88
 
74
89
  # Notify WebSocket server (always update, even if file doesn't exist yet)
@@ -78,6 +93,20 @@ module StaticSiteBuilder
78
93
  puts "\nāœ“ Build complete! Output in #{dist_dir}"
79
94
  end
80
95
 
96
+ # Public method to render partials - no longer needed as ActionView handles this directly
97
+ # ActionView's render method automatically finds and renders partials from app/views
98
+ # This method is kept for backwards compatibility but should not be called
99
+ def render_partial(partial_path, view_context, locals = {})
100
+ # ActionView handles partial rendering automatically through its render method
101
+ # When templates call render 'shared/header', ActionView finds _header.html.erb automatically
102
+ begin
103
+ view_context.render(partial: partial_path, locals: locals)
104
+ rescue ActionView::MissingTemplate => e
105
+ # Convert ActionView's error to our format for backwards compatibility
106
+ raise "Partial not found: #{partial_path} (looked for #{e.path})"
107
+ end
108
+ end
109
+
81
110
  private
82
111
 
83
112
  def load_importmap_config
@@ -116,10 +145,18 @@ module StaticSiteBuilder
116
145
  copy_vendor_files_from_node_modules(dist_dir)
117
146
  end
118
147
 
119
- # Copy CSS
148
+ # Copy CSS (skip if Tailwind is handling it - check for tailwind.config.js)
149
+ # Tailwind outputs directly to dist, so we don't want to overwrite with raw files
150
+ # But we still need to ensure the directory exists for Tailwind to write to
151
+ tailwind_config = @root.join("tailwind.config.js")
120
152
  css_dir = @root.join("app", "assets", "stylesheets")
121
- if css_dir.exist? && css_dir.directory?
122
- dist_css = dist_dir.join("assets", "stylesheets")
153
+ dist_css = dist_dir.join("assets", "stylesheets")
154
+
155
+ if tailwind_config.exist?
156
+ # Tailwind is handling CSS - ensure directory exists but don't copy raw files
157
+ FileUtils.mkdir_p(dist_css)
158
+ elsif css_dir.exist? && css_dir.directory?
159
+ # No Tailwind - copy CSS files normally
123
160
  FileUtils.mkdir_p(dist_css)
124
161
  Dir.glob(css_dir.join("*")).each do |item|
125
162
  FileUtils.cp_r(item, dist_css, preserve: true)
@@ -223,30 +260,11 @@ module StaticSiteBuilder
223
260
  def compile_erb_page(erb_file, page_name, dist_dir, importmap_json_str)
224
261
  puts "Compiling #{page_name}..."
225
262
 
226
- # Read and parse frontmatter
263
+ # Read ERB content - ActionView will process it directly
227
264
  content = File.read(erb_file)
228
- frontmatter = {}
265
+
266
+ # Default layout
229
267
  layout = "application"
230
- js_modules = []
231
-
232
- if content.match?(/^---\s*\n/)
233
- match = content.match(/^---\s*\n(.*?)\n---\s*\n/m)
234
- if match
235
- frontmatter_text = match[1]
236
- frontmatter_text.each_line do |line|
237
- key, value = line.split(":", 2).map(&:strip)
238
- case key
239
- when "layout"
240
- layout = value
241
- when "js"
242
- js_modules = value.split(",").map(&:strip)
243
- else
244
- frontmatter[key] = value
245
- end
246
- end
247
- content = content.sub(/^---\s*\n.*?\n---\s*\n/m, "")
248
- end
249
- end
250
268
 
251
269
  # Load layout - try .html.erb first, then .html
252
270
  layout_file = @root.join("app", "views", "layouts", "#{layout}.html.erb")
@@ -277,14 +295,117 @@ module StaticSiteBuilder
277
295
  end
278
296
  end
279
297
 
280
- # Create binding with variables for ERB
281
- page_binding = binding
282
- page_binding.local_variable_set(:frontmatter, frontmatter)
283
- page_binding.local_variable_set(:js_modules, js_modules)
284
- page_binding.local_variable_set(:importmap_json, importmap_json_str) if importmap_json_str
298
+ # Set current_page based on the file being compiled
299
+ current_page_path = if page_name == 'index.html'
300
+ '/'
301
+ else
302
+ "/#{page_name.gsub(/\.html$/, '')}"
303
+ end
304
+
305
+ # Create ActionView lookup context with view paths
306
+ view_paths = ActionView::PathSet.new([@root.join("app", "views").to_s])
307
+ lookup_context = ActionView::LookupContext.new(view_paths)
308
+
309
+ # Create ActionView::Base instance for rendering using with_empty_template_cache
310
+ # This is the recommended way for standalone ActionView usage
311
+ view_class = ActionView::Base.with_empty_template_cache
312
+ view = view_class.new(lookup_context, {}, self)
313
+
314
+ # Include PageHelpers if available (look in project root)
315
+ # Only require once (first time)
316
+ unless defined?(@page_helpers_loaded)
317
+ begin
318
+ page_helpers_path = @root.join('lib', 'page_helpers.rb')
319
+ if page_helpers_path.exist?
320
+ require page_helpers_path.to_s
321
+ @page_helpers_loaded = true
322
+ end
323
+ rescue LoadError
324
+ # PageHelpers not available, continue without it
325
+ end
326
+ end
327
+
328
+ # Extend view with PageHelpers if available
329
+ if defined?(PageHelpers)
330
+ view.extend(PageHelpers) unless view.singleton_class.included_modules.include?(PageHelpers)
331
+ end
332
+
333
+ # Set instance variables that will be available in templates
334
+ # Pages can set @title, @js_modules, etc. via ERB at the top
335
+ view.instance_variable_set(:@js_modules, [])
336
+ view.instance_variable_set(:@importmap_json, importmap_json_str) if importmap_json_str
337
+ view.instance_variable_set(:@current_page, current_page_path)
338
+ view.instance_variable_set(:@page_content, nil)
339
+
340
+
341
+ # Override render to handle 'footer' -> 'shared/footer' conversion
342
+ # and ensure locals are passed to partials
343
+ view.define_singleton_method(:render) do |options = {}, locals = {}, &block|
344
+ begin
345
+ # Handle string/symbol partial names: render 'footer' -> render 'shared/footer'
346
+ if options.is_a?(String) || options.is_a?(Symbol)
347
+ partial_name = options.to_s
348
+ # If no path separator, assume it's in shared/
349
+ unless partial_name.include?('/')
350
+ partial_name = "shared/#{partial_name}"
351
+ end
352
+ # Merge page locals with any provided locals
353
+ merged_locals = {
354
+ importmap_json: importmap_json_str,
355
+ current_page: current_page_path
356
+ }.merge(locals.is_a?(Hash) ? locals : {})
357
+ super(partial: partial_name, locals: merged_locals, &block)
358
+ elsif options.is_a?(Hash)
359
+ # Handle hash options: render partial: 'footer', locals: {}
360
+ partial_path = options[:partial] || options['partial']
361
+ if partial_path
362
+ # Convert 'footer' to 'shared/footer' if no path
363
+ unless partial_path.to_s.include?('/')
364
+ partial_path = "shared/#{partial_path}"
365
+ end
366
+ # Merge page locals with provided locals
367
+ provided_locals = options[:locals] || options['locals'] || {}
368
+ merged_locals = {
369
+ importmap_json: importmap_json_str,
370
+ current_page: current_page_path
371
+ }.merge(provided_locals)
372
+ super(partial: partial_path, locals: merged_locals, &block)
373
+ else
374
+ # Other render options (template, etc.)
375
+ super(options, locals, &block)
376
+ end
377
+ else
378
+ super(options, locals, &block)
379
+ end
380
+ rescue ActionView::MissingTemplate => e
381
+ # Convert ActionView's error to our format for backwards compatibility
382
+ raise "Partial not found: #{partial_path || partial_name || 'unknown'} (looked for #{e.path})"
383
+ end
384
+ end
285
385
 
286
- # Render ERB content
287
- page_content = ERB.new(content).result(page_binding)
386
+ # Render page content using ActionView
387
+ # Pages can set instance variables via ERB (e.g., <% @title = '...' %>)
388
+ page_template = ActionView::Template.new(
389
+ content,
390
+ "inline:page",
391
+ ActionView::Template::Handlers::ERB.new,
392
+ virtual_path: "pages/#{page_name.gsub(/\.html$/, '')}",
393
+ format: :html,
394
+ locals: [:importmap_json, :current_page]
395
+ )
396
+
397
+ begin
398
+ page_content = view.render(template: page_template, locals: {
399
+ importmap_json: importmap_json_str,
400
+ current_page: current_page_path
401
+ })
402
+ rescue ActionView::Template::Error => e
403
+ # Convert ActionView errors to our format
404
+ if e.cause.is_a?(ActionView::MissingTemplate)
405
+ raise "Partial not found: #{e.cause.path} (looked for #{e.cause.path})"
406
+ end
407
+ raise
408
+ end
288
409
 
289
410
  # Annotate page content if enabled
290
411
  if @annotate_template_file_names
@@ -292,20 +413,65 @@ module StaticSiteBuilder
292
413
  page_content = annotate_template(page_content, relative_template_path.to_s)
293
414
  end
294
415
 
295
- page_binding.local_variable_set(:page_content, page_content)
416
+ # Set title and metadata from PageHelpers before rendering layout
417
+ puts " DEBUG: About to set metadata for #{current_page_path}"
418
+ page_helpers_path = @root.join('lib', 'page_helpers.rb')
419
+ puts " DEBUG: Checking PageHelpers at: #{page_helpers_path} (exists: #{page_helpers_path.exist?})"
420
+ begin
421
+ if page_helpers_path.exist?
422
+ require page_helpers_path.to_s
423
+ pages = ::PageHelpers::PAGES rescue nil
424
+ puts " Pages loaded: #{pages ? 'yes' : 'no'}, keys: #{pages&.keys&.inspect}"
425
+ if pages && pages.is_a?(Hash) && pages.key?(current_page_path)
426
+ metadata = pages[current_page_path]
427
+ view.instance_variable_set(:@title, metadata[:title])
428
+ view.instance_variable_set(:@description, metadata[:description])
429
+ view.instance_variable_set(:@url, metadata[:url])
430
+ view.instance_variable_set(:@image, metadata[:image])
431
+ puts " āœ“ Set metadata for #{current_page_path}: #{metadata[:title]}"
432
+ else
433
+ puts " ⚠ No metadata found for #{current_page_path}"
434
+ end
435
+ end
436
+ rescue => e
437
+ puts " ⚠ Error loading PageHelpers: #{e.class} - #{e.message}"
438
+ puts e.backtrace.first(3)
439
+ end
296
440
 
297
- # Render layout with page content
298
- layout_erb = ERB.new(layout_content)
299
- rendered = layout_erb.result(page_binding)
441
+ # Render layout using ActionView
442
+ # Instance variables set in the page template are available in the layout
443
+ layout_template = ActionView::Template.new(
444
+ layout_content,
445
+ "inline:layout",
446
+ ActionView::Template::Handlers::ERB.new,
447
+ virtual_path: "layouts/#{layout}",
448
+ format: :html,
449
+ locals: [:page_content, :importmap_json, :current_page]
450
+ )
451
+
452
+ # Mark page_content as HTML safe to prevent escaping
453
+ # ActionView will escape strings by default in ERB, so we mark it as safe
454
+ safe_page_content = page_content.respond_to?(:html_safe) ? page_content.html_safe : page_content
455
+
456
+ rendered = view.render(template: layout_template, locals: {
457
+ page_content: safe_page_content,
458
+ importmap_json: importmap_json_str,
459
+ current_page: current_page_path
460
+ })
300
461
 
301
462
  # Annotate layout if enabled
463
+ # Note: We wrap the rendered content without removing existing annotations
464
+ # This preserves page annotations that are already in the content
302
465
  if @annotate_template_file_names && layout_file.exist?
303
466
  relative_layout_path = Pathname.new(layout_file).relative_path_from(@root)
304
- rendered = annotate_template(rendered, relative_layout_path.to_s)
467
+ begin_comment = "<!-- BEGIN #{relative_layout_path} -->"
468
+ end_comment = "<!-- END #{relative_layout_path} -->"
469
+ rendered = "#{begin_comment}\n#{rendered}\n#{end_comment}"
305
470
  end
306
471
 
307
472
  output_path = dist_dir.join(page_name)
308
473
  FileUtils.mkdir_p(output_path.dirname)
474
+ puts " Debug: Writing #{rendered.length} chars to #{output_path}"
309
475
  File.write(output_path, rendered)
310
476
 
311
477
  puts " āœ“ Created #{page_name}"
@@ -371,16 +537,16 @@ module StaticSiteBuilder
371
537
  <head>
372
538
  <meta charset="UTF-8">
373
539
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
374
- <title><%= frontmatter['title'] || 'Site' %></title>
540
+ <title><%= @title || 'Site' %></title>
375
541
  <link rel="stylesheet" href="/assets/stylesheets/application.css">
376
542
  </head>
377
543
  <body>
378
544
  <%= page_content %>
379
- <% if defined?(importmap_json) && importmap_json %>
380
- <script type="importmap"><%= importmap_json %></script>
545
+ <% if defined?(@importmap_json) && @importmap_json %>
546
+ <script type="importmap"><%= @importmap_json %></script>
381
547
  <% end %>
382
- <% if js_modules && !js_modules.empty? %>
383
- <% js_modules.each do |module_name| %>
548
+ <% if @js_modules && !@js_modules.empty? %>
549
+ <% @js_modules.each do |module_name| %>
384
550
  <script type="module">import "<%= module_name %>";</script>
385
551
  <% end %>
386
552
  <% else %>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StaticSiteBuilder
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: static-site-builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lukasz Czapiewski
@@ -9,6 +9,20 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: actionview
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '8.0'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: base64
14
28
  requirement: !ruby/object:Gem::Requirement