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 +4 -4
- data/CHANGELOG.md +39 -0
- data/lib/generator.rb +127 -9
- data/lib/static_site_builder/builder.rb +214 -48
- data/lib/static_site_builder/version.rb +1 -1
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cfa2d9ecc9c90e973fd3cecd6c04e2aad09a80fa29ba0bd78a95b524206986c4
|
|
4
|
+
data.tar.gz: 2394ef787382dec487390d4b05a1add37a27e3e9154f032f82d9972fae25924e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 -
|
|
638
|
-
|
|
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 (
|
|
697
|
-
task :all => [:
|
|
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
|
|
736
|
+
desc "Build JavaScript assets"
|
|
702
737
|
task :assets do
|
|
703
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
|
263
|
+
# Read ERB content - ActionView will process it directly
|
|
227
264
|
content = File.read(erb_file)
|
|
228
|
-
|
|
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
|
-
#
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
287
|
-
|
|
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
|
-
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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><%=
|
|
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 &&
|
|
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 %>
|
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.
|
|
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
|