wytch 0.1.0 → 0.3.0
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 +17 -0
- data/README.md +23 -1
- data/exe/wytch +6 -0
- data/lib/wytch/builder.rb +75 -0
- data/lib/wytch/cli.rb +127 -0
- data/lib/wytch/content_loader.rb +54 -0
- data/lib/wytch/once.rb +40 -0
- data/lib/wytch/page.rb +136 -0
- data/lib/wytch/reload_coordinator.rb +68 -0
- data/lib/wytch/server.rb +93 -0
- data/lib/wytch/site.rb +97 -0
- data/lib/wytch/site_code_loader_middleware.rb +37 -0
- data/lib/wytch/templates/Gemfile.tt +12 -0
- data/lib/wytch/templates/config.rb.tt +6 -0
- data/lib/wytch/templates/content/feed.rb.tt +4 -0
- data/lib/wytch/templates/content/index.rb.tt +6 -0
- data/lib/wytch/templates/content/posts/hello-world.rb.tt +12 -0
- data/lib/wytch/templates/content/sitemap.rb.tt +4 -0
- data/lib/wytch/templates/frontend/Main.res.tt +2 -0
- data/lib/wytch/templates/frontend/main.css.tt +1 -0
- data/lib/wytch/templates/gitignore.tt +4 -0
- data/lib/wytch/templates/package.json.tt +19 -0
- data/lib/wytch/templates/public/robots.txt.tt +2 -0
- data/lib/wytch/templates/rescript.json.tt +18 -0
- data/lib/wytch/templates/src/site/feed_helper.rb.tt +17 -0
- data/lib/wytch/templates/src/site/feed_view.rb.tt +42 -0
- data/lib/wytch/templates/src/site/home_view.rb.tt +29 -0
- data/lib/wytch/templates/src/site/layout.rb.tt +34 -0
- data/lib/wytch/templates/src/site/page.rb.tt +9 -0
- data/lib/wytch/templates/src/site/post_helpers.rb.tt +18 -0
- data/lib/wytch/templates/src/site/post_view.rb.tt +21 -0
- data/lib/wytch/templates/src/site/sitemap_helper.rb.tt +17 -0
- data/lib/wytch/templates/src/site/sitemap_view.rb.tt +27 -0
- data/lib/wytch/templates/vite.config.js.tt +26 -0
- data/lib/wytch/version.rb +2 -1
- data/lib/wytch.rb +52 -2
- data/website/.gitignore +4 -0
- data/website/Gemfile +8 -0
- data/website/Gemfile.lock +63 -0
- data/website/assets/Main.res +2 -0
- data/website/assets/main.css +1 -0
- data/website/bin/generate_api_docs +44 -0
- data/website/config.rb +6 -0
- data/website/content/api/wytch/builder.rb +6 -0
- data/website/content/api/wytch/cli.rb +6 -0
- data/website/content/api/wytch/contentloader.rb +6 -0
- data/website/content/api/wytch/error.rb +6 -0
- data/website/content/api/wytch/once.rb +6 -0
- data/website/content/api/wytch/page.rb +6 -0
- data/website/content/api/wytch/reloadcoordinator.rb +6 -0
- data/website/content/api/wytch/server.rb +6 -0
- data/website/content/api/wytch/site.rb +6 -0
- data/website/content/api/wytch/sitecodeloadermiddleware.rb +6 -0
- data/website/content/index.rb +6 -0
- data/website/content/sitemap.rb +4 -0
- data/website/package.json +19 -0
- data/website/public/robots.txt +2 -0
- data/website/rescript.json +18 -0
- data/website/src/wytch_site/api_class_view.rb +90 -0
- data/website/src/wytch_site/home_view.rb +33 -0
- data/website/src/wytch_site/layout.rb +34 -0
- data/website/src/wytch_site/page.rb +18 -0
- data/website/src/wytch_site/sitemap_helper.rb +17 -0
- data/website/src/wytch_site/sitemap_view.rb +27 -0
- data/website/vite.config.js +26 -0
- metadata +164 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7aa017b3a2d6ed1b64cde7b4a73f312222d64a630a365ad71db68f4babc28ce5
|
|
4
|
+
data.tar.gz: fba837a626f3c4dfcc3525d03127ada6b62b30b367e31da4c5eef3721c7b264a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b70258ec6436a644dee4ecb637dc0bcad929428c7803d4272b7eef34b8bb0620b024c683a2a77f41e1235c05f5c1713b188a374bc7d050bb77a571bd924db7f0
|
|
7
|
+
data.tar.gz: 98a84928a3cf7bd5aff50e99225d6b944da43ca69a44c2bb94a10ee8bc5ba424365ca228378f2f2472757b25632f86b7b5bea0aaa458e075100e2b96f1f87985
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2025-12-7
|
|
4
|
+
|
|
5
|
+
- Add YARD documentation to all classes and methods
|
|
6
|
+
- Add UTF-8 charset meta tag to generated site layouts
|
|
7
|
+
- Fix index page building to `build/index.html` instead of `build/index/index.html`
|
|
8
|
+
- Update CLI output to reflect single-command build process
|
|
9
|
+
|
|
10
|
+
## [0.2.0] - 2025-12-7
|
|
11
|
+
|
|
12
|
+
- Add Vite integration for asset pipeline (CSS, ReScript)
|
|
13
|
+
- Add blog scaffolding with posts, sitemap, and Atom feed
|
|
14
|
+
- Add Layout component for consistent page structure
|
|
15
|
+
- `wytch build` now automatically runs `npm run build` for assets
|
|
16
|
+
- Add static file serving from `public/` directory
|
|
17
|
+
- Add hot-reloading for site code and content during development
|
|
18
|
+
- Add documentation (README usage guide)
|
|
19
|
+
|
|
3
20
|
## [0.1.0] - 2025-10-29
|
|
4
21
|
|
|
5
22
|
- Initial release
|
data/README.md
CHANGED
|
@@ -11,7 +11,29 @@ gem install wytch
|
|
|
11
11
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Generate a new site like this:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
wytch new my-site
|
|
18
|
+
cd my-site
|
|
19
|
+
bundle install
|
|
20
|
+
npm install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Run the development servers with:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm run dev # Runs the vite dev server for assets
|
|
27
|
+
wytch server # Runes the Wytch dev server
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then visit `http://localhost:6969` to see your site.
|
|
31
|
+
|
|
32
|
+
Once you are ready to build for production:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
wytch build # Builds assets and generates your static site in the build directory.
|
|
36
|
+
```
|
|
15
37
|
|
|
16
38
|
## Development
|
|
17
39
|
|
data/exe/wytch
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Wytch
|
|
6
|
+
# Builds the static site by rendering all pages to the output directory.
|
|
7
|
+
#
|
|
8
|
+
# The Builder is responsible for the production build process:
|
|
9
|
+
# 1. Loading site configuration and content
|
|
10
|
+
# 2. Rendering each page to HTML
|
|
11
|
+
# 3. Copying static files from public/
|
|
12
|
+
# 4. Integrating Vite-built assets
|
|
13
|
+
#
|
|
14
|
+
# @example Building via CLI
|
|
15
|
+
# $ wytch build
|
|
16
|
+
#
|
|
17
|
+
# @example Building programmatically
|
|
18
|
+
# Wytch::Builder.new.build
|
|
19
|
+
class Builder
|
|
20
|
+
# @return [String] the output directory for built files
|
|
21
|
+
OUTPUT_DIR = "build"
|
|
22
|
+
|
|
23
|
+
# Builds the entire site.
|
|
24
|
+
#
|
|
25
|
+
# Sets RACK_ENV to "production", loads the site configuration,
|
|
26
|
+
# renders all pages to HTML files, and copies static assets.
|
|
27
|
+
#
|
|
28
|
+
# @return [void]
|
|
29
|
+
def build
|
|
30
|
+
ENV["RACK_ENV"] = "production"
|
|
31
|
+
Site.load!
|
|
32
|
+
|
|
33
|
+
Wytch.site.site_code_loader.eager_load
|
|
34
|
+
Wytch.site.load_content
|
|
35
|
+
|
|
36
|
+
FileUtils.mkdir(OUTPUT_DIR) unless Dir.exist?(OUTPUT_DIR)
|
|
37
|
+
|
|
38
|
+
Wytch.site.pages.values.each do |page|
|
|
39
|
+
build_path = File.join(OUTPUT_DIR, page.build_path)
|
|
40
|
+
|
|
41
|
+
puts "Building #{page.path} → #{build_path}"
|
|
42
|
+
|
|
43
|
+
FileUtils.mkdir_p File.dirname(build_path)
|
|
44
|
+
|
|
45
|
+
File.write build_path, page.render
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
copy_public_files
|
|
49
|
+
copy_vite_assets
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Copies files from public/ to the output directory.
|
|
55
|
+
#
|
|
56
|
+
# @return [void]
|
|
57
|
+
def copy_public_files
|
|
58
|
+
return unless Dir.exist?("public")
|
|
59
|
+
|
|
60
|
+
FileUtils.cp_r "public/.", OUTPUT_DIR, verbose: true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Reports on Vite assets in the output directory.
|
|
64
|
+
# Vite builds directly to build/assets, so no copying is needed.
|
|
65
|
+
#
|
|
66
|
+
# @return [void]
|
|
67
|
+
def copy_vite_assets
|
|
68
|
+
vite_output = File.join(OUTPUT_DIR, "assets")
|
|
69
|
+
return unless Dir.exist?(vite_output)
|
|
70
|
+
|
|
71
|
+
# Vite already builds to build/assets, so assets are already in place
|
|
72
|
+
puts "Vite assets ready at #{vite_output}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/wytch/cli.rb
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Wytch
|
|
6
|
+
# Command-line interface for Wytch.
|
|
7
|
+
#
|
|
8
|
+
# Provides commands for creating, developing, and building Wytch sites.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# $ wytch new my-site # Create a new site
|
|
12
|
+
# $ wytch server # Start development server
|
|
13
|
+
# $ wytch build # Build for production
|
|
14
|
+
class CLI < Thor
|
|
15
|
+
include Thor::Actions
|
|
16
|
+
|
|
17
|
+
# Returns the path to template files used by the generator.
|
|
18
|
+
#
|
|
19
|
+
# @return [String] path to templates directory
|
|
20
|
+
def self.source_root
|
|
21
|
+
File.expand_path("templates", __dir__)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc "new NAME", "Create a new Wytch site"
|
|
25
|
+
method_option :local, type: :boolean, default: false, desc: "Use local Wytch gem for development"
|
|
26
|
+
# Creates a new Wytch site with the given name.
|
|
27
|
+
#
|
|
28
|
+
# Generates a complete site structure including:
|
|
29
|
+
# - Configuration files (Gemfile, config.rb, package.json, etc.)
|
|
30
|
+
# - Content directory with sample pages
|
|
31
|
+
# - Source directory with views and layouts
|
|
32
|
+
# - Asset pipeline setup (Vite, ReScript, Tailwind)
|
|
33
|
+
#
|
|
34
|
+
# @param name [String] the name/directory for the new site
|
|
35
|
+
# @return [void]
|
|
36
|
+
def new(name)
|
|
37
|
+
@local_wytch_path = File.expand_path("../..", __dir__) if options[:local]
|
|
38
|
+
|
|
39
|
+
# Extract site name and create module constant
|
|
40
|
+
@site_name = File.basename(name)
|
|
41
|
+
@site_module = classify(@site_name)
|
|
42
|
+
|
|
43
|
+
empty_directory(name)
|
|
44
|
+
template("Gemfile.tt", "#{name}/Gemfile")
|
|
45
|
+
template("config.rb.tt", "#{name}/config.rb")
|
|
46
|
+
template("gitignore.tt", "#{name}/.gitignore")
|
|
47
|
+
|
|
48
|
+
# Asset pipeline configuration
|
|
49
|
+
template("package.json.tt", "#{name}/package.json")
|
|
50
|
+
template("vite.config.js.tt", "#{name}/vite.config.js")
|
|
51
|
+
template("rescript.json.tt", "#{name}/rescript.json")
|
|
52
|
+
|
|
53
|
+
# Assets directory
|
|
54
|
+
empty_directory("#{name}/assets")
|
|
55
|
+
template("frontend/Main.res.tt", "#{name}/assets/Main.res")
|
|
56
|
+
template("frontend/main.css.tt", "#{name}/assets/main.css")
|
|
57
|
+
|
|
58
|
+
# Content directory
|
|
59
|
+
empty_directory("#{name}/content")
|
|
60
|
+
template("content/index.rb.tt", "#{name}/content/index.rb")
|
|
61
|
+
empty_directory("#{name}/content/posts")
|
|
62
|
+
template("content/posts/hello-world.rb.tt", "#{name}/content/posts/hello-world.rb")
|
|
63
|
+
template("content/sitemap.rb.tt", "#{name}/content/sitemap.rb")
|
|
64
|
+
template("content/feed.rb.tt", "#{name}/content/feed.rb")
|
|
65
|
+
|
|
66
|
+
# Src directory with namespaced code
|
|
67
|
+
empty_directory("#{name}/src")
|
|
68
|
+
empty_directory("#{name}/src/#{@site_name}")
|
|
69
|
+
template("src/site/page.rb.tt", "#{name}/src/#{@site_name}/page.rb")
|
|
70
|
+
template("src/site/layout.rb.tt", "#{name}/src/#{@site_name}/layout.rb")
|
|
71
|
+
template("src/site/home_view.rb.tt", "#{name}/src/#{@site_name}/home_view.rb")
|
|
72
|
+
template("src/site/post_view.rb.tt", "#{name}/src/#{@site_name}/post_view.rb")
|
|
73
|
+
template("src/site/post_helpers.rb.tt", "#{name}/src/#{@site_name}/post_helpers.rb")
|
|
74
|
+
template("src/site/sitemap_view.rb.tt", "#{name}/src/#{@site_name}/sitemap_view.rb")
|
|
75
|
+
template("src/site/sitemap_helper.rb.tt", "#{name}/src/#{@site_name}/sitemap_helper.rb")
|
|
76
|
+
template("src/site/feed_view.rb.tt", "#{name}/src/#{@site_name}/feed_view.rb")
|
|
77
|
+
template("src/site/feed_helper.rb.tt", "#{name}/src/#{@site_name}/feed_helper.rb")
|
|
78
|
+
|
|
79
|
+
# Public directory for static assets
|
|
80
|
+
empty_directory("#{name}/public")
|
|
81
|
+
template("public/robots.txt.tt", "#{name}/public/robots.txt")
|
|
82
|
+
|
|
83
|
+
say "Created new Wytch site in #{name}/", :green
|
|
84
|
+
say "\nNext steps:", :green
|
|
85
|
+
say " cd #{name}"
|
|
86
|
+
say " bundle install"
|
|
87
|
+
say " npm install"
|
|
88
|
+
say "\nTo start developing:"
|
|
89
|
+
say " npm run dev # Start Vite dev server (Terminal 1)"
|
|
90
|
+
say " wytch server # Start Wytch dev server (Terminal 2)"
|
|
91
|
+
say "\nTo build for production:"
|
|
92
|
+
say " wytch build # Build assets and static site to build/"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
desc "server", "Start a development server"
|
|
96
|
+
method_option :port, type: :numeric, default: 6969, aliases: "-p", desc: "Port to run the server on"
|
|
97
|
+
method_option :host, type: :string, default: "localhost", aliases: "-h", desc: "Host to bind the server to"
|
|
98
|
+
# Starts the development server with hot reloading.
|
|
99
|
+
#
|
|
100
|
+
# @return [void]
|
|
101
|
+
def server
|
|
102
|
+
Server.new(options).start
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
desc "build", "Build the static site"
|
|
106
|
+
# Builds the site for production.
|
|
107
|
+
#
|
|
108
|
+
# First runs `npm run build` to compile assets with Vite,
|
|
109
|
+
# then renders all pages to static HTML in the build/ directory.
|
|
110
|
+
#
|
|
111
|
+
# @return [void]
|
|
112
|
+
def build
|
|
113
|
+
system("npm run build") || abort("Asset build failed")
|
|
114
|
+
Builder.new.build
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# Converts a snake_case name to PascalCase.
|
|
120
|
+
#
|
|
121
|
+
# @param name [String] the name to classify
|
|
122
|
+
# @return [String] the PascalCase version
|
|
123
|
+
def classify(name)
|
|
124
|
+
name.split("_").map(&:capitalize).join
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wytch
|
|
4
|
+
# Discovers and loads content files into Page objects.
|
|
5
|
+
#
|
|
6
|
+
# The ContentLoader scans the content directory for Ruby files and
|
|
7
|
+
# instantiates Page objects for each one. It uses the configured
|
|
8
|
+
# page_class from the Site.
|
|
9
|
+
#
|
|
10
|
+
# @see Wytch::Site#content_dir
|
|
11
|
+
# @see Wytch::Site#page_class
|
|
12
|
+
class ContentLoader
|
|
13
|
+
# Loads all content files and returns a hash of pages.
|
|
14
|
+
#
|
|
15
|
+
# Scans the content directory recursively for .rb files,
|
|
16
|
+
# creates a Page instance for each, and returns them keyed by path.
|
|
17
|
+
#
|
|
18
|
+
# @return [Hash{String => Page}] pages keyed by their URL path
|
|
19
|
+
def load_content
|
|
20
|
+
pages = {}
|
|
21
|
+
|
|
22
|
+
Dir.glob(File.join("**", "*.rb"), base: content_dir).each do |file_path|
|
|
23
|
+
page = load_page(file_path)
|
|
24
|
+
pages[page.path] = page
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
pages
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Creates a Page instance from a content file.
|
|
33
|
+
#
|
|
34
|
+
# @param file_path [String] path to the content file, relative to content_dir
|
|
35
|
+
# @return [Page] the loaded page
|
|
36
|
+
def load_page(file_path)
|
|
37
|
+
page_class.new(file_path:)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns the configured page class.
|
|
41
|
+
#
|
|
42
|
+
# @return [Class] the page class to instantiate
|
|
43
|
+
def page_class
|
|
44
|
+
Object.const_get(Wytch.site.page_class)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns the content directory path.
|
|
48
|
+
#
|
|
49
|
+
# @return [String] the content directory
|
|
50
|
+
def content_dir
|
|
51
|
+
Wytch.site.content_dir
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/wytch/once.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wytch
|
|
4
|
+
# A thread-safe utility for executing a block exactly once.
|
|
5
|
+
#
|
|
6
|
+
# Once ensures that a given block of code is executed only one time,
|
|
7
|
+
# even when called from multiple threads. After the first execution,
|
|
8
|
+
# subsequent calls are no-ops.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# setup = Once.new { puts "Initializing..." }
|
|
12
|
+
# setup.call # prints "Initializing..."
|
|
13
|
+
# setup.call # does nothing
|
|
14
|
+
# setup.call # does nothing
|
|
15
|
+
class Once
|
|
16
|
+
# Creates a new Once instance with the given block.
|
|
17
|
+
#
|
|
18
|
+
# @yield the block to execute once
|
|
19
|
+
def initialize(&block)
|
|
20
|
+
@block = block
|
|
21
|
+
@mutex = Mutex.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Executes the block if it hasn't been executed yet.
|
|
25
|
+
#
|
|
26
|
+
# Thread-safe: if multiple threads call this simultaneously,
|
|
27
|
+
# only one will execute the block.
|
|
28
|
+
#
|
|
29
|
+
# @return [void]
|
|
30
|
+
def call
|
|
31
|
+
@mutex&.synchronize do
|
|
32
|
+
return unless @mutex
|
|
33
|
+
|
|
34
|
+
@block.call
|
|
35
|
+
|
|
36
|
+
@mutex = nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/wytch/page.rb
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wytch
|
|
4
|
+
# Represents a single page in a Wytch site.
|
|
5
|
+
#
|
|
6
|
+
# Pages are defined by Ruby content files in the content directory. Each content
|
|
7
|
+
# file is evaluated in the context of a Page instance, allowing it to set metadata
|
|
8
|
+
# and configure the view class.
|
|
9
|
+
#
|
|
10
|
+
# @example A typical content file (content/about.rb)
|
|
11
|
+
# view MySite::AboutView
|
|
12
|
+
# add MySite::PageHelpers
|
|
13
|
+
#
|
|
14
|
+
# @metadata[:title] = "About Us"
|
|
15
|
+
# @metadata[:description] = "Learn more about our company"
|
|
16
|
+
#
|
|
17
|
+
# @example Accessing page data in a view
|
|
18
|
+
# class MySite::AboutView < Phlex::HTML
|
|
19
|
+
# def initialize(page)
|
|
20
|
+
# @page = page
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# def view_template
|
|
24
|
+
# h1 { @page.metadata[:title] }
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
class Page
|
|
28
|
+
# Creates a new page by loading and evaluating a content file.
|
|
29
|
+
#
|
|
30
|
+
# @param file_path [String] path to the content file, relative to the content directory
|
|
31
|
+
def initialize(file_path:)
|
|
32
|
+
@file_path = file_path
|
|
33
|
+
@metadata = {}
|
|
34
|
+
@view_class = nil
|
|
35
|
+
|
|
36
|
+
source_path = File.join(Wytch.site.content_dir, @file_path)
|
|
37
|
+
|
|
38
|
+
instance_eval File.read(source_path), source_path
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Hash] arbitrary metadata set by the content file
|
|
42
|
+
attr_reader :metadata
|
|
43
|
+
|
|
44
|
+
# Renders the page using its configured view class.
|
|
45
|
+
#
|
|
46
|
+
# @return [String] the rendered HTML
|
|
47
|
+
def render
|
|
48
|
+
@view_class.new(self).call
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns the URL path for this page.
|
|
52
|
+
#
|
|
53
|
+
# @return [String] the URL path (e.g., "/" for index, "/about" for about.rb)
|
|
54
|
+
def path
|
|
55
|
+
if virtual_path == "index"
|
|
56
|
+
"/"
|
|
57
|
+
else
|
|
58
|
+
"/#{virtual_path}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns the virtual path derived from the file path.
|
|
63
|
+
#
|
|
64
|
+
# @return [String] the path without directory prefix or .rb suffix
|
|
65
|
+
def virtual_path
|
|
66
|
+
@file_path.delete_prefix("#{Wytch.site.content_dir}/").delete_suffix(".rb")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns the output file path for the built page.
|
|
70
|
+
#
|
|
71
|
+
# @return [String] the build path (e.g., "about/index.html")
|
|
72
|
+
def build_path
|
|
73
|
+
if virtual_path == "index"
|
|
74
|
+
"index.html"
|
|
75
|
+
else
|
|
76
|
+
"#{virtual_path}/index.html"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Extends this page instance with a helper module.
|
|
81
|
+
# Called from content files to add helper methods.
|
|
82
|
+
#
|
|
83
|
+
# @param helper_module [Module] the module to extend this page with
|
|
84
|
+
# @return [void]
|
|
85
|
+
#
|
|
86
|
+
# @example
|
|
87
|
+
# add MySite::PostHelpers
|
|
88
|
+
def add(helper_module)
|
|
89
|
+
extend helper_module
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Sets the view class for rendering this page.
|
|
93
|
+
# Called from content files to specify which Phlex view to use.
|
|
94
|
+
#
|
|
95
|
+
# @param view_class [Class] a Phlex view class that accepts the page in its initializer
|
|
96
|
+
# @return [void]
|
|
97
|
+
#
|
|
98
|
+
# @example
|
|
99
|
+
# view MySite::PostView
|
|
100
|
+
def view(view_class)
|
|
101
|
+
@view_class = view_class
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Whether this page should be included in the sitemap.
|
|
105
|
+
# Override in subclasses to exclude pages.
|
|
106
|
+
#
|
|
107
|
+
# @return [Boolean] true by default
|
|
108
|
+
def include_in_sitemap?
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The last modification date for sitemap generation.
|
|
113
|
+
# Override in subclasses to provide actual dates.
|
|
114
|
+
#
|
|
115
|
+
# @return [Date, Time, nil] the last modified date, or nil if unknown
|
|
116
|
+
def last_modified
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The change frequency hint for sitemap generation.
|
|
121
|
+
# Override in subclasses to provide hints like "daily", "weekly", etc.
|
|
122
|
+
#
|
|
123
|
+
# @return [String, nil] the change frequency, or nil if unknown
|
|
124
|
+
def change_frequency
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The priority hint for sitemap generation.
|
|
129
|
+
# Override in subclasses to provide a value between 0.0 and 1.0.
|
|
130
|
+
#
|
|
131
|
+
# @return [Float, nil] the priority, or nil if unknown
|
|
132
|
+
def priority
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent/atomic/read_write_lock"
|
|
4
|
+
require "listen"
|
|
5
|
+
|
|
6
|
+
module Wytch
|
|
7
|
+
# Coordinates hot reloading of site code and content during development.
|
|
8
|
+
#
|
|
9
|
+
# The ReloadCoordinator watches the site code and content directories for
|
|
10
|
+
# changes using the Listen gem. When files change, it marks the appropriate
|
|
11
|
+
# component as dirty and reloads it on the next request.
|
|
12
|
+
#
|
|
13
|
+
# It uses a read-write lock to ensure thread-safe reloading while allowing
|
|
14
|
+
# concurrent reads during page rendering.
|
|
15
|
+
#
|
|
16
|
+
# @see Wytch::SiteCodeLoaderMiddleware
|
|
17
|
+
class ReloadCoordinator
|
|
18
|
+
# @return [Concurrent::ReadWriteLock] lock for coordinating reloads
|
|
19
|
+
attr_reader :reload_lock
|
|
20
|
+
|
|
21
|
+
# Creates a new ReloadCoordinator and sets up file watchers.
|
|
22
|
+
def initialize
|
|
23
|
+
@reload_lock = Concurrent::ReadWriteLock.new
|
|
24
|
+
|
|
25
|
+
@site_code_dirty = true
|
|
26
|
+
@content_dirty = true
|
|
27
|
+
|
|
28
|
+
@start_site_code_listener = Once.new do
|
|
29
|
+
Listen.to(Wytch.site.site_code_path) do
|
|
30
|
+
@site_code_dirty = true
|
|
31
|
+
end.start
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@start_content_listener = Once.new do
|
|
35
|
+
Listen.to(Wytch.site.content_dir) do
|
|
36
|
+
@content_dirty = true
|
|
37
|
+
end.start
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Reloads site code and/or content if changes have been detected.
|
|
42
|
+
#
|
|
43
|
+
# Starts file listeners on first call. If site code has changed, reloads
|
|
44
|
+
# both site code (via Zeitwerk) and content. If only content has changed,
|
|
45
|
+
# reloads just the content.
|
|
46
|
+
#
|
|
47
|
+
# @return [void]
|
|
48
|
+
def reload!
|
|
49
|
+
@start_site_code_listener&.call
|
|
50
|
+
@start_content_listener&.call
|
|
51
|
+
|
|
52
|
+
return unless @site_code_dirty || @content_dirty
|
|
53
|
+
|
|
54
|
+
reload_lock.with_write_lock do
|
|
55
|
+
if @site_code_dirty
|
|
56
|
+
# Site code changed: reload site code then reload content
|
|
57
|
+
@site_code_dirty = false
|
|
58
|
+
Wytch.site.site_code_loader.reload
|
|
59
|
+
Wytch.site.load_content
|
|
60
|
+
elsif @content_dirty
|
|
61
|
+
# Only content changed: just reload content
|
|
62
|
+
@content_dirty = false
|
|
63
|
+
Wytch.site.load_content
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/wytch/server.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack"
|
|
4
|
+
|
|
5
|
+
module Wytch
|
|
6
|
+
# Development server for Wytch sites.
|
|
7
|
+
#
|
|
8
|
+
# The Server provides a local development environment with:
|
|
9
|
+
# - Hot reloading of site code and content
|
|
10
|
+
# - Static file serving from public/
|
|
11
|
+
# - Page rendering on each request
|
|
12
|
+
#
|
|
13
|
+
# @example Starting via CLI
|
|
14
|
+
# $ wytch server
|
|
15
|
+
# $ wytch server --port 3000 --host 0.0.0.0
|
|
16
|
+
#
|
|
17
|
+
# @example Starting programmatically
|
|
18
|
+
# Wytch::Server.new(port: 3000).start
|
|
19
|
+
class Server
|
|
20
|
+
# @return [Hash] server options (port, host)
|
|
21
|
+
attr_reader :options
|
|
22
|
+
|
|
23
|
+
# Creates a new server instance.
|
|
24
|
+
#
|
|
25
|
+
# @param options [Hash] server options
|
|
26
|
+
# @option options [Integer] :port the port to listen on (default: 6969)
|
|
27
|
+
# @option options [String] :host the host to bind to (default: "localhost")
|
|
28
|
+
def initialize(options = {})
|
|
29
|
+
@options = options
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Starts the development server.
|
|
33
|
+
#
|
|
34
|
+
# Loads the site configuration, starts file watchers for hot reloading,
|
|
35
|
+
# and begins serving requests. This method blocks until the server is stopped.
|
|
36
|
+
#
|
|
37
|
+
# @return [void]
|
|
38
|
+
def start
|
|
39
|
+
ENV["RACK_ENV"] = "development"
|
|
40
|
+
Site.load!
|
|
41
|
+
|
|
42
|
+
require "puma"
|
|
43
|
+
require "puma/server"
|
|
44
|
+
|
|
45
|
+
port = options[:port] || 6969
|
|
46
|
+
host = options[:host] || "localhost"
|
|
47
|
+
|
|
48
|
+
puts "Starting Wytch development server on http://#{host}:#{port}"
|
|
49
|
+
puts "Press Ctrl+C to stop"
|
|
50
|
+
|
|
51
|
+
server = Puma::Server.new(app)
|
|
52
|
+
server.add_tcp_listener(host, port)
|
|
53
|
+
server.run.join
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Builds the Rack application stack.
|
|
59
|
+
#
|
|
60
|
+
# @return [Rack::Builder] the configured Rack app
|
|
61
|
+
def app
|
|
62
|
+
base_app = lambda { |env|
|
|
63
|
+
path = env["PATH_INFO"]
|
|
64
|
+
|
|
65
|
+
# Otherwise try to serve a page
|
|
66
|
+
page = Wytch.site.pages[path]
|
|
67
|
+
|
|
68
|
+
if page
|
|
69
|
+
body = page.render
|
|
70
|
+
[
|
|
71
|
+
200,
|
|
72
|
+
{"content-type" => "text/html"},
|
|
73
|
+
[body]
|
|
74
|
+
]
|
|
75
|
+
else
|
|
76
|
+
[
|
|
77
|
+
404,
|
|
78
|
+
{"content-type" => "text/html"},
|
|
79
|
+
["<html><body><h1>404 Not Found</h1><p>Page not found: #{path}</p></body></html>"]
|
|
80
|
+
]
|
|
81
|
+
end
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
reload_coordinator = ReloadCoordinator.new
|
|
85
|
+
|
|
86
|
+
Rack::Builder.new do
|
|
87
|
+
use Rack::Static, urls: [""], root: "public", cascade: true
|
|
88
|
+
|
|
89
|
+
run SiteCodeLoaderMiddleware.new(base_app, reload_coordinator)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|