static-site-builder 0.0.1 → 0.1.4

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: 4ec7765f3cc618a791a6d655fed75fe39f43a09d9c697cc71135d17160dbd27f
4
+ data.tar.gz: 74ace48bd713ba712607509baf63a80095cd610395be622b59c3e552336f10df
5
5
  SHA512:
6
- metadata.gz: 96ace8067d05e208f9d26061962558fc9bfef30196e703be13249573612fb674ab496e6e052522e5c8ab6006b39b6825108d89a360323b5c4e6d2e9c02badc96
7
- data.tar.gz: 2656f4592bf359cd186284ebbba773e83772fda781121e9515cee27d9f72b7fa7e6520c23858464842da95c9fab197782c7a4b510ef2b7cf3cbb0b2e7207be8d
6
+ metadata.gz: 358643b7c2cb4bd56693c18fa4f57114da85b519a1423e319a55d1b4d7ee0d3190768985225539e6c65f9eed044d96fced689d5464d39fb0db85238dbcb4e6d4
7
+ data.tar.gz: a8ab43edfdac947a34e0222c181037213120eb1f9dea91c924f218e974908c51491135ce475aadde6552bedd65698cf05a54e18af60b73669b6c6fe7134cb84a
data/CHANGELOG.md CHANGED
@@ -5,6 +5,66 @@ 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.4] - 2025-11-22
9
+
10
+ ### Added
11
+ - Generator now automatically creates `lib/page_helpers.rb` with `PageHelpers::PAGES` structure for page metadata
12
+ - Generator now automatically creates `config/sitemap.rb` for sitemap generation
13
+ - `sitemap_generator` gem is now automatically included in generated Gemfiles
14
+ - `build:sitemap` task is automatically added to generated Rakefiles
15
+ - Sitemap generation is integrated into `build:all` task
16
+
17
+ ### Changed
18
+ - Page metadata is now managed via `PageHelpers::PAGES` hash instead of frontmatter
19
+ - README updated to reflect `PageHelpers::PAGES` approach (frontmatter example removed)
20
+ - ActionView requirement changed from `>= 8.0` to `~> 7.1` for Ruby 3.1 compatibility
21
+ - Generated layouts now use `@title` and `@js_modules` instance variables instead of frontmatter
22
+
23
+ ### Fixed
24
+ - Fixed `js_modules` variable reference in generated layouts (now uses `@js_modules`)
25
+ - Fixed PageHelpers metadata loading to occur before page content rendering, allowing partials to access `@title`, `@description`, etc.
26
+ - Removed all frontmatter parsing code and references
27
+ - Updated all specs to use `PageHelpers::PAGES` instead of frontmatter
28
+
29
+ ## [0.1.3] - 2025-11-22
30
+
31
+ ### Added
32
+ - Integrated ActionView 8+ for proper Rails-style partial rendering
33
+ - Support for Rails-style render syntax: `render 'shared/header'` and `render partial: 'shared/header'`
34
+ - Support for passing locals to partials: `render partial: 'shared/header', locals: { title: 'Hello' }`
35
+ - Nested partial support (partials can render other partials)
36
+ - Multiple partials on the same page now work correctly
37
+
38
+ ### Changed
39
+ - Replaced raw ERB implementation with ActionView::Base for template rendering
40
+ - Render method now uses ActionView's rendering system, matching Rails behaviour exactly
41
+ - Partials automatically receive page variables (@js_modules, importmap_json, current_page)
42
+ - Improved error messages for missing partials (converted from ActionView format for backwards compatibility)
43
+ - Template annotations now preserve both page and layout annotations correctly
44
+
45
+ ### Fixed
46
+ - Fixed issue where nested partials (partials rendering other partials) would fail or produce incorrect output
47
+ - Fixed issue where multiple partials on the same page would only render the last one
48
+ - Fixed template annotations being stripped when layout annotations were added
49
+
50
+ ## [0.1.2] - 2025-11-22
51
+
52
+ ### Fixed
53
+ - CSS directory is now always created when Tailwind handles CSS compilation, preventing 404 errors for stylesheets
54
+ - Build process now updates files in place instead of cleaning and recreating the dist directory, preventing 404 errors during rebuilds in development mode
55
+ - Fixed race condition where pages would return 404 when code changes triggered rebuilds
56
+
57
+ ## [0.1.1] - 2025-11-21
58
+
59
+ ### Added
60
+ - `render` helper method for ERB templates to include partials
61
+ - Support for rendering partials from `app/views/shared/` directory
62
+ - Partial files should be named with `_` prefix (e.g., `_header.html.erb`)
63
+ - Partials have access to page variables (@js_modules, etc.)
64
+
65
+ ### Changed
66
+ - Improved ERB compilation to support partial rendering
67
+
8
68
  ## [0.0.1] - 2025-11-21
9
69
 
10
70
  ### Added
@@ -15,7 +75,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
75
  - Support for multiple JavaScript bundlers (Importmap, ESBuild, Webpack, Vite, None)
16
76
  - Support for multiple CSS frameworks (TailwindCSS, shadcn/ui, Plain CSS)
17
77
  - Support for multiple JavaScript frameworks (Stimulus, React, Vue, Alpine.js, Vanilla)
18
- - Frontmatter parsing for ERB pages
78
+ - Page metadata via `PageHelpers::PAGES` hash
19
79
  - Layout support with nested layouts
20
80
  - Importmap JSON generation
21
81
  - Asset copying (JavaScript, CSS, vendor files, static files)
data/README.md CHANGED
@@ -48,11 +48,12 @@ A clean project structure that depends on gems:
48
48
 
49
49
  ```
50
50
  my-site/
51
- ├── Gemfile # Dependencies (static-site-builder, importmap-rails, etc.)
51
+ ├── Gemfile # Dependencies (static-site-builder, sitemap_generator, etc.)
52
52
  ├── package.json # JS dependencies (if needed)
53
- ├── Rakefile # Build tasks
53
+ ├── Rakefile # Build tasks (includes sitemap generation)
54
54
  ├── config/
55
- └── importmap.rb # Importmap config (if using importmap)
55
+ ├── importmap.rb # Importmap config (if using importmap)
56
+ │ └── sitemap.rb # Sitemap generation config
56
57
  ├── app/
57
58
  │ ├── views/
58
59
  │ │ ├── layouts/
@@ -61,7 +62,8 @@ my-site/
61
62
  │ ├── javascript/
62
63
  │ └── assets/
63
64
  └── lib/
64
- └── site_builder.rb # Compiles your site
65
+ ├── site_builder.rb # Compiles your site
66
+ └── page_helpers.rb # Page metadata (title, description, etc.)
65
67
  ```
66
68
 
67
69
  ## How It Works
@@ -109,18 +111,32 @@ my-site/
109
111
 
110
112
  ### Using ERB Templates
111
113
 
112
- Create pages in `app/views/pages/` with frontmatter:
114
+ Create pages in `app/views/pages/`:
113
115
 
114
116
  ```erb
115
- ---
116
- title: My Page
117
- description: A great page
118
- ---
119
-
120
117
  <h1><%= @title %></h1>
121
118
  <p><%= @description %></p>
122
119
  ```
123
120
 
121
+ Page metadata is automatically configured in `lib/page_helpers.rb` (generated automatically):
122
+
123
+ ```ruby
124
+ module PageHelpers
125
+ PAGES = {
126
+ '/' => {
127
+ title: 'My Page',
128
+ description: 'A great page',
129
+ url: 'https://example.com',
130
+ image: 'https://example.com/image.jpg',
131
+ priority: 1.0,
132
+ changefreq: 'weekly'
133
+ }
134
+ }.freeze
135
+ end
136
+ ```
137
+
138
+ The builder automatically loads metadata from `PageHelpers::PAGES` and sets `@title`, `@description`, `@url`, and `@image` instance variables for use in your templates. This metadata is also used by the `sitemap_generator` gem for generating sitemaps.
139
+
124
140
  Use layouts in `app/views/layouts/application.html.erb`:
125
141
 
126
142
  ```erb
@@ -214,6 +230,18 @@ Install components and use them in your templates:
214
230
  npx shadcn-ui@latest add button
215
231
  ```
216
232
 
233
+ ### Generating Sitemaps
234
+
235
+ Sitemap generation is automatically configured when you generate a new site. The `sitemap_generator` gem is included in the Gemfile, and `config/sitemap.rb` is automatically created.
236
+
237
+ The sitemap is generated from your `PageHelpers::PAGES` metadata during `rake build:all`. Update `config/sitemap.rb` to set your domain:
238
+
239
+ ```ruby
240
+ SitemapGenerator::Sitemap.default_host = 'https://yourdomain.com'
241
+ ```
242
+
243
+ The sitemap will be generated in `dist/sitemaps/sitemap.xml.gz` during the build process.
244
+
217
245
  ## Examples
218
246
 
219
247
  ### ERB + Importmap + Stimulus + TailwindCSS
data/lib/generator.rb CHANGED
@@ -68,8 +68,11 @@ module StaticSiteBuilder
68
68
  create_config_files
69
69
  create_app_structure
70
70
  create_build_files
71
+ create_page_helpers
72
+ create_sitemap_config
71
73
  create_example_pages
72
74
  create_readme
75
+ create_gitignore
73
76
 
74
77
  puts "\n✓ Site generated successfully!"
75
78
  puts "\nNext steps:"
@@ -103,7 +106,8 @@ module StaticSiteBuilder
103
106
  gems = [
104
107
  "rake",
105
108
  "static-site-builder",
106
- "webrick" # Required for dev server (removed from stdlib in Ruby 3.0+)
109
+ "webrick", # Required for dev server (removed from stdlib in Ruby 3.0+)
110
+ "sitemap_generator" # For generating sitemaps from PageHelpers::PAGES
107
111
  ]
108
112
  gems << "importmap-rails" if @options[:js_bundler] == "importmap"
109
113
  gems << "phlex-rails" if @options[:template_engine] == "phlex"
@@ -185,6 +189,78 @@ module StaticSiteBuilder
185
189
  create_rails_config if @options[:edit_rails]
186
190
  end
187
191
 
192
+ def create_page_helpers
193
+ content = <<~RUBY
194
+ # frozen_string_literal: true
195
+
196
+ module PageHelpers
197
+ # Page metadata configuration
198
+ # The builder automatically loads this and sets @title, @description, @url, and @image
199
+ # instance variables for use in your templates.
200
+ # This metadata is also used by sitemap_generator for generating sitemaps.
201
+ PAGES = {
202
+ '/' => {
203
+ title: 'Home',
204
+ description: 'Welcome to my site',
205
+ url: 'https://example.com',
206
+ image: 'https://example.com/image.jpg',
207
+ priority: 1.0,
208
+ changefreq: 'weekly'
209
+ }
210
+ }.freeze
211
+
212
+ def page_title(path = nil)
213
+ path ||= @current_page
214
+ PAGES[path]&.fetch(:title) || 'Site'
215
+ end
216
+
217
+ def page_description(path = nil)
218
+ path ||= @current_page
219
+ PAGES[path]&.fetch(:description) || 'A static site'
220
+ end
221
+
222
+ def page_url(path = nil)
223
+ path ||= @current_page
224
+ PAGES[path]&.fetch(:url) || 'https://example.com'
225
+ end
226
+
227
+ def page_image(path = nil)
228
+ path ||= @current_page
229
+ PAGES[path]&.fetch(:image) || 'https://example.com/image.jpg'
230
+ end
231
+ end
232
+ RUBY
233
+
234
+ write_file("lib/page_helpers.rb", content)
235
+ end
236
+
237
+ def create_sitemap_config
238
+ content = <<~RUBY
239
+ # frozen_string_literal: true
240
+
241
+ require 'sitemap_generator'
242
+ require_relative '../lib/page_helpers'
243
+
244
+ # Configure sitemap generator
245
+ # Update default_host to your actual domain
246
+ SitemapGenerator::Sitemap.default_host = 'https://example.com'
247
+ SitemapGenerator::Sitemap.sitemaps_path = 'sitemaps'
248
+ SitemapGenerator::Sitemap.public_path = 'dist'
249
+
250
+ # Generate sitemap from PageHelpers::PAGES
251
+ SitemapGenerator::Sitemap.create do
252
+ PageHelpers::PAGES.each do |path, metadata|
253
+ add path,
254
+ lastmod: Time.now,
255
+ priority: metadata[:priority] || 0.5,
256
+ changefreq: metadata[:changefreq] || 'weekly'
257
+ end
258
+ end
259
+ RUBY
260
+
261
+ write_file("config/sitemap.rb", content)
262
+ end
263
+
188
264
  def create_importmap_config
189
265
  content = <<~RUBY
190
266
  # frozen_string_literal: true
@@ -409,7 +485,7 @@ module StaticSiteBuilder
409
485
  <head>
410
486
  <meta charset="UTF-8">
411
487
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
412
- <title><%= frontmatter['title'] || 'Site' %></title>
488
+ <title><%= @title || 'Site' %></title>
413
489
  <link rel="stylesheet" href="/assets/stylesheets/application.css">
414
490
  </head>
415
491
  <body>
@@ -469,8 +545,8 @@ module StaticSiteBuilder
469
545
  case @options[:js_bundler]
470
546
  when "importmap"
471
547
  <<~ERB
472
- <% if js_modules && !js_modules.empty? %>
473
- <% js_modules.each do |module_name| %>
548
+ <% if @js_modules.present? %>
549
+ <% @js_modules.each do |module_name| %>
474
550
  <script type="module">import "<%= module_name %>";</script>
475
551
  <% end %>
476
552
  <% else %>
@@ -580,6 +656,18 @@ module StaticSiteBuilder
580
656
  @tailwind base;
581
657
  @tailwind components;
582
658
  @tailwind utilities;
659
+
660
+ @layer base {
661
+ html {
662
+ scroll-behavior: smooth;
663
+ }
664
+ }
665
+
666
+ @layer utilities {
667
+ section[id] {
668
+ scroll-margin-top: 5rem;
669
+ }
670
+ }
583
671
  CSS
584
672
  when "shadcn"
585
673
  write_file("app/assets/stylesheets/application.css", <<~CSS)
@@ -614,6 +702,7 @@ module StaticSiteBuilder
614
702
  require "webrick"
615
703
  require "fileutils"
616
704
  require "static_site_builder/websocket_server"
705
+ require "json"
617
706
 
618
707
  port = ENV["PORT"] || 3000
619
708
  ws_port = ENV["WS_PORT"] || 3001
@@ -629,13 +718,33 @@ module StaticSiteBuilder
629
718
  ENV["WS_PORT"] = ws_port.to_s
630
719
  Rake::Task["build:all"].invoke
631
720
 
721
+ # Check if we need to run Tailwind CSS watch (after initial build)
722
+ tailwind_pid = nil
723
+ package_json_path = Pathname.new(Dir.pwd).join("package.json")
724
+ if package_json_path.exist?
725
+ package_json = JSON.parse(File.read(package_json_path))
726
+ if package_json.dig("scripts", "watch:css")
727
+ puts "🎨 Starting Tailwind CSS watch mode..."
728
+ tailwind_pid = spawn("npm", "run", "watch:css", :err => File::NULL, :out => File::NULL)
729
+ # Touch the source file to trigger Tailwind watch to process CSS immediately
730
+ css_source = Pathname.new(Dir.pwd).join("app", "assets", "stylesheets", "application.css")
731
+ if css_source.exist?
732
+ FileUtils.touch(css_source)
733
+ end
734
+ # Give Tailwind a moment to process CSS
735
+ sleep 1.5
736
+ end
737
+ end
738
+
632
739
  puts "\n🚀 Starting development server at http://localhost:#{port}"
633
740
  puts "📡 WebSocket server at ws://localhost:#{ws_port}"
634
741
  puts "📝 Watching for changes... (Ctrl+C to stop)"
635
742
  puts "🔄 Live reload enabled - pages will auto-refresh on changes\n"
636
743
 
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}
744
+ # Simple file watcher - rebuild HTML when non-CSS files change
745
+ # CSS changes are handled by Tailwind watch, so we skip rebuild for CSS files
746
+ # When HTML rebuilds, it cleans dist, so we need to rebuild CSS immediately after
747
+ 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
748
  watcher_pid = spawn("ruby", "-e", watcher_code, :err => File::NULL)
640
749
 
641
750
  # Start web server
@@ -648,6 +757,7 @@ module StaticSiteBuilder
648
757
  trap("INT") do
649
758
  puts "\n\nShutting down..."
650
759
  Process.kill("TERM", watcher_pid) if watcher_pid
760
+ Process.kill("TERM", tailwind_pid) if tailwind_pid
651
761
  ws_server.stop
652
762
  server.shutdown
653
763
  end
@@ -693,27 +803,61 @@ module StaticSiteBuilder
693
803
  require "pathname"
694
804
 
695
805
  namespace :build do
696
- desc "Build everything (assets + HTML)"
697
- task :all => [:assets, :html] do
806
+ desc "Build everything (HTML + CSS + Sitemap)"
807
+ task :all => [:html, :css, :sitemap] do
698
808
  puts "\\n✓ Build complete!"
699
809
  end
700
810
 
701
- desc "Build JavaScript/CSS assets"
811
+ desc "Build JavaScript assets"
702
812
  task :assets do
703
- sh "npm run build" if File.exist?("package.json")
813
+ if File.exist?("package.json")
814
+ package_json = JSON.parse(File.read("package.json"))
815
+ build_script = package_json.dig("scripts", "build")
816
+ # Only run if build script exists and doesn't include CSS (CSS handled separately)
817
+ if build_script && !build_script.include?("build:css")
818
+ sh "npm run build"
819
+ end
820
+ end
704
821
  end
705
822
 
706
823
  desc "Compile all pages to static HTML"
707
- task :html do
824
+ task :html => [:assets] do
708
825
  load "lib/site_builder.rb"
709
826
  end
710
827
 
828
+ desc "Build CSS (runs after HTML so dist directory exists)"
829
+ task :css do
830
+ if File.exist?("package.json")
831
+ package_json = JSON.parse(File.read("package.json"))
832
+ if package_json.dig("scripts", "build:css")
833
+ sh "npm run build:css"
834
+ end
835
+ elsif File.exist?("tailwind.config.js")
836
+ # Build CSS even if no package.json (standalone Tailwind)
837
+ if system("which tailwindcss > /dev/null 2>&1")
838
+ FileUtils.mkdir_p("dist/assets/stylesheets")
839
+ sh "tailwindcss -i ./app/assets/stylesheets/application.css -o ./dist/assets/stylesheets/application.css --minify"
840
+ end
841
+ end
842
+ end
843
+
711
844
  desc "Clean dist directory"
712
845
  task :clean do
713
846
  dist_dir = Pathname.new(Dir.pwd).join("dist")
714
847
  FileUtils.rm_rf(dist_dir) if dist_dir.exist?
715
848
  puts "Cleaned \#{dist_dir}"
716
849
  end
850
+
851
+ desc "Build for production/release (cleans dist directory first)"
852
+ task :production do
853
+ ENV["PRODUCTION"] = "true"
854
+ Rake::Task["build:all"].invoke
855
+ end
856
+
857
+ desc "Generate sitemap from PageHelpers::PAGES"
858
+ task :sitemap do
859
+ require './config/sitemap'
860
+ end
717
861
  end
718
862
 
719
863
  namespace :dev do
@@ -734,8 +878,8 @@ module StaticSiteBuilder
734
878
  require "pathname"
735
879
 
736
880
  namespace :build do
737
- desc "Build everything (HTML)"
738
- task :all => [:html] do
881
+ desc "Build everything (HTML + Sitemap)"
882
+ task :all => [:html, :sitemap] do
739
883
  puts "\\n✓ Build complete!"
740
884
  end
741
885
 
@@ -750,6 +894,17 @@ module StaticSiteBuilder
750
894
  FileUtils.rm_rf(dist_dir) if dist_dir.exist?
751
895
  puts "Cleaned \#{dist_dir}"
752
896
  end
897
+
898
+ desc "Build for production/release (cleans dist directory first)"
899
+ task :production do
900
+ ENV["PRODUCTION"] = "true"
901
+ Rake::Task["build:all"].invoke
902
+ end
903
+
904
+ desc "Generate sitemap from PageHelpers::PAGES"
905
+ task :sitemap do
906
+ require './config/sitemap'
907
+ end
753
908
  end
754
909
 
755
910
  namespace :dev do
@@ -887,7 +1042,7 @@ module StaticSiteBuilder
887
1042
  end
888
1043
 
889
1044
  def build_script
890
- case @options[:js_bundler]
1045
+ js_build = case @options[:js_bundler]
891
1046
  when "esbuild"
892
1047
  "node esbuild.config.js"
893
1048
  when "webpack"
@@ -895,7 +1050,20 @@ module StaticSiteBuilder
895
1050
  when "vite"
896
1051
  "vite build"
897
1052
  else
898
- "echo 'No JS bundling needed'"
1053
+ nil
1054
+ end
1055
+
1056
+ css_build = if needs_css_build?
1057
+ "npm run build:css"
1058
+ else
1059
+ nil
1060
+ end
1061
+
1062
+ builds = [js_build, css_build].compact
1063
+ if builds.empty?
1064
+ "echo 'No bundling needed'"
1065
+ else
1066
+ builds.join(" && ")
899
1067
  end
900
1068
  end
901
1069
 
@@ -924,6 +1092,41 @@ module StaticSiteBuilder
924
1092
  @options[:css_framework] == "tailwindcss" || @options[:css_framework] == "shadcn"
925
1093
  end
926
1094
 
1095
+ def create_gitignore
1096
+ content = <<~GITIGNORE
1097
+ # Dependencies
1098
+ /.bundle/
1099
+ /vendor/bundle
1100
+ /node_modules/
1101
+
1102
+ # Build artifacts
1103
+ *.gem
1104
+ *.gemspec.bak
1105
+ /dist/
1106
+ /tmp/
1107
+ /coverage/
1108
+
1109
+ # Test artifacts
1110
+ /.rspec_status
1111
+
1112
+ # IDE
1113
+ /.idea/
1114
+ /.vscode/
1115
+ *.swp
1116
+ *.swo
1117
+ *~
1118
+
1119
+ # OS
1120
+ .DS_Store
1121
+ Thumbs.db
1122
+
1123
+ # Logs
1124
+ *.log
1125
+ GITIGNORE
1126
+
1127
+ write_file(".gitignore", content)
1128
+ end
1129
+
927
1130
  def write_file(path, content)
928
1131
  file_path = @app_path.join(path)
929
1132
  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,135 @@ 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
285
304
 
286
- # Render ERB content
287
- page_content = ERB.new(content).result(page_binding)
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
+ # Set title and metadata from PageHelpers BEFORE rendering page content
341
+ # This ensures partials rendered within the page have access to metadata
342
+ page_helpers_path = @root.join('lib', 'page_helpers.rb')
343
+ begin
344
+ if page_helpers_path.exist?
345
+ require page_helpers_path.to_s
346
+ pages = ::PageHelpers::PAGES rescue nil
347
+ if pages && pages.is_a?(Hash) && pages.key?(current_page_path)
348
+ metadata = pages[current_page_path]
349
+ view.instance_variable_set(:@title, metadata[:title])
350
+ view.instance_variable_set(:@description, metadata[:description])
351
+ view.instance_variable_set(:@url, metadata[:url])
352
+ view.instance_variable_set(:@image, metadata[:image])
353
+ end
354
+ end
355
+ rescue => e
356
+ # Silently continue if PageHelpers can't be loaded
357
+ end
358
+
359
+ # Override render to handle 'footer' -> 'shared/footer' conversion
360
+ # and ensure locals are passed to partials
361
+ view.define_singleton_method(:render) do |options = {}, locals = {}, &block|
362
+ begin
363
+ # Handle string/symbol partial names: render 'footer' -> render 'shared/footer'
364
+ if options.is_a?(String) || options.is_a?(Symbol)
365
+ partial_name = options.to_s
366
+ # If no path separator, assume it's in shared/
367
+ unless partial_name.include?('/')
368
+ partial_name = "shared/#{partial_name}"
369
+ end
370
+ # Merge page locals with any provided locals
371
+ merged_locals = {
372
+ importmap_json: importmap_json_str,
373
+ current_page: current_page_path
374
+ }.merge(locals.is_a?(Hash) ? locals : {})
375
+ super(partial: partial_name, locals: merged_locals, &block)
376
+ elsif options.is_a?(Hash)
377
+ # Handle hash options: render partial: 'footer', locals: {}
378
+ partial_path = options[:partial] || options['partial']
379
+ if partial_path
380
+ # Convert 'footer' to 'shared/footer' if no path
381
+ unless partial_path.to_s.include?('/')
382
+ partial_path = "shared/#{partial_path}"
383
+ end
384
+ # Merge page locals with provided locals
385
+ provided_locals = options[:locals] || options['locals'] || {}
386
+ merged_locals = {
387
+ importmap_json: importmap_json_str,
388
+ current_page: current_page_path
389
+ }.merge(provided_locals)
390
+ super(partial: partial_path, locals: merged_locals, &block)
391
+ else
392
+ # Other render options (template, etc.)
393
+ super(options, locals, &block)
394
+ end
395
+ else
396
+ super(options, locals, &block)
397
+ end
398
+ rescue ActionView::MissingTemplate => e
399
+ # Convert ActionView's error to our format for backwards compatibility
400
+ raise "Partial not found: #{partial_path || partial_name || 'unknown'} (looked for #{e.path})"
401
+ end
402
+ end
403
+
404
+ # Render page content using ActionView
405
+ # Pages can set instance variables via ERB (e.g., <% @title = '...' %>)
406
+ page_template = ActionView::Template.new(
407
+ content,
408
+ "inline:page",
409
+ ActionView::Template::Handlers::ERB.new,
410
+ virtual_path: "pages/#{page_name.gsub(/\.html$/, '')}",
411
+ format: :html,
412
+ locals: [:importmap_json, :current_page]
413
+ )
414
+
415
+ begin
416
+ page_content = view.render(template: page_template, locals: {
417
+ importmap_json: importmap_json_str,
418
+ current_page: current_page_path
419
+ })
420
+ rescue ActionView::Template::Error => e
421
+ # Convert ActionView errors to our format
422
+ if e.cause.is_a?(ActionView::MissingTemplate)
423
+ raise "Partial not found: #{e.cause.path} (looked for #{e.cause.path})"
424
+ end
425
+ raise
426
+ end
288
427
 
289
428
  # Annotate page content if enabled
290
429
  if @annotate_template_file_names
@@ -292,20 +431,40 @@ module StaticSiteBuilder
292
431
  page_content = annotate_template(page_content, relative_template_path.to_s)
293
432
  end
294
433
 
295
- page_binding.local_variable_set(:page_content, page_content)
296
-
297
- # Render layout with page content
298
- layout_erb = ERB.new(layout_content)
299
- rendered = layout_erb.result(page_binding)
434
+ # Render layout using ActionView
435
+ # Instance variables set in the page template are available in the layout
436
+ layout_template = ActionView::Template.new(
437
+ layout_content,
438
+ "inline:layout",
439
+ ActionView::Template::Handlers::ERB.new,
440
+ virtual_path: "layouts/#{layout}",
441
+ format: :html,
442
+ locals: [:page_content, :importmap_json, :current_page]
443
+ )
444
+
445
+ # Mark page_content as HTML safe to prevent escaping
446
+ # ActionView will escape strings by default in ERB, so we mark it as safe
447
+ safe_page_content = page_content.respond_to?(:html_safe) ? page_content.html_safe : page_content
448
+
449
+ rendered = view.render(template: layout_template, locals: {
450
+ page_content: safe_page_content,
451
+ importmap_json: importmap_json_str,
452
+ current_page: current_page_path
453
+ })
300
454
 
301
455
  # Annotate layout if enabled
456
+ # Note: We wrap the rendered content without removing existing annotations
457
+ # This preserves page annotations that are already in the content
302
458
  if @annotate_template_file_names && layout_file.exist?
303
459
  relative_layout_path = Pathname.new(layout_file).relative_path_from(@root)
304
- rendered = annotate_template(rendered, relative_layout_path.to_s)
460
+ begin_comment = "<!-- BEGIN #{relative_layout_path} -->"
461
+ end_comment = "<!-- END #{relative_layout_path} -->"
462
+ rendered = "#{begin_comment}\n#{rendered}\n#{end_comment}"
305
463
  end
306
464
 
307
465
  output_path = dist_dir.join(page_name)
308
466
  FileUtils.mkdir_p(output_path.dirname)
467
+ puts " Debug: Writing #{rendered.length} chars to #{output_path}"
309
468
  File.write(output_path, rendered)
310
469
 
311
470
  puts " ✓ Created #{page_name}"
@@ -371,16 +530,16 @@ module StaticSiteBuilder
371
530
  <head>
372
531
  <meta charset="UTF-8">
373
532
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
374
- <title><%= frontmatter['title'] || 'Site' %></title>
533
+ <title><%= @title || 'Site' %></title>
375
534
  <link rel="stylesheet" href="/assets/stylesheets/application.css">
376
535
  </head>
377
536
  <body>
378
537
  <%= page_content %>
379
- <% if defined?(importmap_json) && importmap_json %>
380
- <script type="importmap"><%= importmap_json %></script>
538
+ <% if defined?(@importmap_json) && @importmap_json %>
539
+ <script type="importmap"><%= @importmap_json %></script>
381
540
  <% end %>
382
- <% if js_modules && !js_modules.empty? %>
383
- <% js_modules.each do |module_name| %>
541
+ <% if @js_modules && !@js_modules.empty? %>
542
+ <% @js_modules.each do |module_name| %>
384
543
  <script type="module">import "<%= module_name %>";</script>
385
544
  <% end %>
386
545
  <% 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.4"
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.4
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: '7.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '7.1'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: base64
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -114,7 +128,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
114
128
  requirements:
115
129
  - - ">="
116
130
  - !ruby/object:Gem::Version
117
- version: '3.0'
131
+ version: '3.1'
118
132
  required_rubygems_version: !ruby/object:Gem::Requirement
119
133
  requirements:
120
134
  - - ">="