wytch 0.2.0 → 0.4.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/lib/wytch/builder.rb +27 -0
  4. data/lib/wytch/cli.rb +35 -4
  5. data/lib/wytch/content_loader.rb +24 -0
  6. data/lib/wytch/once.rb +20 -0
  7. data/lib/wytch/page.rb +76 -1
  8. data/lib/wytch/reload_coordinator.rb +19 -0
  9. data/lib/wytch/server.rb +28 -0
  10. data/lib/wytch/site.rb +49 -0
  11. data/lib/wytch/site_code_loader_middleware.rb +19 -0
  12. data/lib/wytch/sitemap_helper.rb +35 -0
  13. data/lib/wytch/sitemap_view.rb +41 -0
  14. data/lib/wytch/templates/content/sitemap.rb.tt +2 -2
  15. data/lib/wytch/templates/src/site/layout.rb.tt +1 -0
  16. data/lib/wytch/version.rb +2 -1
  17. data/lib/wytch.rb +29 -0
  18. data/website/.gitignore +4 -0
  19. data/website/Gemfile +8 -0
  20. data/website/Gemfile.lock +63 -0
  21. data/website/assets/Main.res +2 -0
  22. data/website/assets/main.css +1 -0
  23. data/website/bin/generate_api_docs +44 -0
  24. data/website/config.rb +6 -0
  25. data/website/content/api/wytch/builder.rb +6 -0
  26. data/website/content/api/wytch/cli.rb +6 -0
  27. data/website/content/api/wytch/contentloader.rb +6 -0
  28. data/website/content/api/wytch/error.rb +6 -0
  29. data/website/content/api/wytch/once.rb +6 -0
  30. data/website/content/api/wytch/page.rb +6 -0
  31. data/website/content/api/wytch/reloadcoordinator.rb +6 -0
  32. data/website/content/api/wytch/server.rb +6 -0
  33. data/website/content/api/wytch/site.rb +6 -0
  34. data/website/content/api/wytch/sitecodeloadermiddleware.rb +6 -0
  35. data/website/content/index.rb +6 -0
  36. data/website/content/sitemap.rb +4 -0
  37. data/website/package.json +19 -0
  38. data/website/public/robots.txt +2 -0
  39. data/website/rescript.json +18 -0
  40. data/website/src/wytch_site/api_class_view.rb +90 -0
  41. data/website/src/wytch_site/home_view.rb +33 -0
  42. data/website/src/wytch_site/layout.rb +34 -0
  43. data/website/src/wytch_site/page.rb +18 -0
  44. data/{lib/wytch/templates/src/site/sitemap_helper.rb.tt → website/src/wytch_site/sitemap_helper.rb} +1 -1
  45. data/{lib/wytch/templates/src/site/sitemap_view.rb.tt → website/src/wytch_site/sitemap_view.rb} +1 -1
  46. data/website/vite.config.js +26 -0
  47. metadata +32 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 662c7632994e850e71fce70842f153b11d7e56f8877df6156094c070213a68a6
4
- data.tar.gz: 546b863eb417cd7ada91d725a1def5662aae4182e9c53762ab98758c574f103b
3
+ metadata.gz: 0437b28a3f15b9b8c8b038d704007549d041d037a06423bdc5e7e707d2f0406a
4
+ data.tar.gz: d3c6f678b5146c515096c32a0ef61bef367b6af875df257455699db790019223
5
5
  SHA512:
6
- metadata.gz: 9a8a7bc87f19981aa54f33132f5cb5e9adf7799c380187f2b5db8a0756feab7f2c6e158eeaeeaef2d6bccb95188f8ff5e561e66b5bf4c8dda70df56859f8c137
7
- data.tar.gz: ac6aae6cf98f82c74dd1b11ba1d814d03cbbb9b9ab85c6c2f738fc300632ff99602c571e436554e58b11aa7d47fb94c99f9532e41eb77116ccd7fe37a9a56dca
6
+ metadata.gz: 1c9b98d2db33c4b4b1ae078ce992d1d8ff7965b5ace303cfa8f49148c52cd0f4564c1d36b2ddad6a46e548e27914bc70b8488aaf5ad51a009a084a037c710a1a
7
+ data.tar.gz: cbfefe11f6b4b6a74534cb150b136f90d370f71a11e8be5ca3f21e926292272233c1a723d96b11fc1b374239257205c3d71c0c3bcae8cb8bda2f5659585add88
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2025-12-7
4
+
5
+ - Move sitemap generation to Wytch core (`Wytch::SitemapView` and `Wytch::SitemapHelper`)
6
+
7
+ ## [0.3.0] - 2025-12-7
8
+
9
+ - Add YARD documentation to all classes and methods
10
+ - Add UTF-8 charset meta tag to generated site layouts
11
+ - Fix index page building to `build/index.html` instead of `build/index/index.html`
12
+ - Update CLI output to reflect single-command build process
13
+
14
+ ## [0.2.0] - 2025-12-7
15
+
16
+ - Add Vite integration for asset pipeline (CSS, ReScript)
17
+ - Add blog scaffolding with posts, sitemap, and Atom feed
18
+ - Add Layout component for consistent page structure
19
+ - `wytch build` now automatically runs `npm run build` for assets
20
+ - Add static file serving from `public/` directory
21
+ - Add hot-reloading for site code and content during development
22
+ - Add documentation (README usage guide)
23
+
3
24
  ## [0.1.0] - 2025-10-29
4
25
 
5
26
  - Initial release
data/lib/wytch/builder.rb CHANGED
@@ -3,9 +3,29 @@
3
3
  require "fileutils"
4
4
 
5
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
6
19
  class Builder
20
+ # @return [String] the output directory for built files
7
21
  OUTPUT_DIR = "build"
8
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]
9
29
  def build
10
30
  ENV["RACK_ENV"] = "production"
11
31
  Site.load!
@@ -31,12 +51,19 @@ module Wytch
31
51
 
32
52
  private
33
53
 
54
+ # Copies files from public/ to the output directory.
55
+ #
56
+ # @return [void]
34
57
  def copy_public_files
35
58
  return unless Dir.exist?("public")
36
59
 
37
60
  FileUtils.cp_r "public/.", OUTPUT_DIR, verbose: true
38
61
  end
39
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]
40
67
  def copy_vite_assets
41
68
  vite_output = File.join(OUTPUT_DIR, "assets")
42
69
  return unless Dir.exist?(vite_output)
data/lib/wytch/cli.rb CHANGED
@@ -3,15 +3,36 @@
3
3
  require "thor"
4
4
 
5
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
6
14
  class CLI < Thor
7
15
  include Thor::Actions
8
16
 
17
+ # Returns the path to template files used by the generator.
18
+ #
19
+ # @return [String] path to templates directory
9
20
  def self.source_root
10
21
  File.expand_path("templates", __dir__)
11
22
  end
12
23
 
13
24
  desc "new NAME", "Create a new Wytch site"
14
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]
15
36
  def new(name)
16
37
  @local_wytch_path = File.expand_path("../..", __dir__) if options[:local]
17
38
 
@@ -50,8 +71,6 @@ module Wytch
50
71
  template("src/site/home_view.rb.tt", "#{name}/src/#{@site_name}/home_view.rb")
51
72
  template("src/site/post_view.rb.tt", "#{name}/src/#{@site_name}/post_view.rb")
52
73
  template("src/site/post_helpers.rb.tt", "#{name}/src/#{@site_name}/post_helpers.rb")
53
- template("src/site/sitemap_view.rb.tt", "#{name}/src/#{@site_name}/sitemap_view.rb")
54
- template("src/site/sitemap_helper.rb.tt", "#{name}/src/#{@site_name}/sitemap_helper.rb")
55
74
  template("src/site/feed_view.rb.tt", "#{name}/src/#{@site_name}/feed_view.rb")
56
75
  template("src/site/feed_helper.rb.tt", "#{name}/src/#{@site_name}/feed_helper.rb")
57
76
 
@@ -68,18 +87,26 @@ module Wytch
68
87
  say " npm run dev # Start Vite dev server (Terminal 1)"
69
88
  say " wytch server # Start Wytch dev server (Terminal 2)"
70
89
  say "\nTo build for production:"
71
- say " npm run build # Build assets with Vite"
72
- say " wytch build # Build static site"
90
+ say " wytch build # Build assets and static site to build/"
73
91
  end
74
92
 
75
93
  desc "server", "Start a development server"
76
94
  method_option :port, type: :numeric, default: 6969, aliases: "-p", desc: "Port to run the server on"
77
95
  method_option :host, type: :string, default: "localhost", aliases: "-h", desc: "Host to bind the server to"
96
+ # Starts the development server with hot reloading.
97
+ #
98
+ # @return [void]
78
99
  def server
79
100
  Server.new(options).start
80
101
  end
81
102
 
82
103
  desc "build", "Build the static site"
104
+ # Builds the site for production.
105
+ #
106
+ # First runs `npm run build` to compile assets with Vite,
107
+ # then renders all pages to static HTML in the build/ directory.
108
+ #
109
+ # @return [void]
83
110
  def build
84
111
  system("npm run build") || abort("Asset build failed")
85
112
  Builder.new.build
@@ -87,6 +114,10 @@ module Wytch
87
114
 
88
115
  private
89
116
 
117
+ # Converts a snake_case name to PascalCase.
118
+ #
119
+ # @param name [String] the name to classify
120
+ # @return [String] the PascalCase version
90
121
  def classify(name)
91
122
  name.split("_").map(&:capitalize).join
92
123
  end
@@ -1,7 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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
4
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
5
19
  def load_content
6
20
  pages = {}
7
21
 
@@ -15,14 +29,24 @@ module Wytch
15
29
 
16
30
  private
17
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
18
36
  def load_page(file_path)
19
37
  page_class.new(file_path:)
20
38
  end
21
39
 
40
+ # Returns the configured page class.
41
+ #
42
+ # @return [Class] the page class to instantiate
22
43
  def page_class
23
44
  Object.const_get(Wytch.site.page_class)
24
45
  end
25
46
 
47
+ # Returns the content directory path.
48
+ #
49
+ # @return [String] the content directory
26
50
  def content_dir
27
51
  Wytch.site.content_dir
28
52
  end
data/lib/wytch/once.rb CHANGED
@@ -1,12 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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
4
15
  class Once
16
+ # Creates a new Once instance with the given block.
17
+ #
18
+ # @yield the block to execute once
5
19
  def initialize(&block)
6
20
  @block = block
7
21
  @mutex = Mutex.new
8
22
  end
9
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]
10
30
  def call
11
31
  @mutex&.synchronize do
12
32
  return unless @mutex
data/lib/wytch/page.rb CHANGED
@@ -1,7 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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
4
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
5
31
  def initialize(file_path:)
6
32
  @file_path = file_path
7
33
  @metadata = {}
@@ -12,12 +38,19 @@ module Wytch
12
38
  instance_eval File.read(source_path), source_path
13
39
  end
14
40
 
41
+ # @return [Hash] arbitrary metadata set by the content file
15
42
  attr_reader :metadata
16
43
 
44
+ # Renders the page using its configured view class.
45
+ #
46
+ # @return [String] the rendered HTML
17
47
  def render
18
48
  @view_class.new(self).call
19
49
  end
20
50
 
51
+ # Returns the URL path for this page.
52
+ #
53
+ # @return [String] the URL path (e.g., "/" for index, "/about" for about.rb)
21
54
  def path
22
55
  if virtual_path == "index"
23
56
  "/"
@@ -26,34 +59,76 @@ module Wytch
26
59
  end
27
60
  end
28
61
 
62
+ # Returns the virtual path derived from the file path.
63
+ #
64
+ # @return [String] the path without directory prefix or .rb suffix
29
65
  def virtual_path
30
66
  @file_path.delete_prefix("#{Wytch.site.content_dir}/").delete_suffix(".rb")
31
67
  end
32
68
 
69
+ # Returns the output file path for the built page.
70
+ #
71
+ # @return [String] the build path (e.g., "about/index.html")
33
72
  def build_path
34
- "#{virtual_path}/index.html"
73
+ if virtual_path == "index"
74
+ "index.html"
75
+ else
76
+ "#{virtual_path}/index.html"
77
+ end
35
78
  end
36
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
37
88
  def add(helper_module)
38
89
  extend helper_module
39
90
  end
40
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
41
100
  def view(view_class)
42
101
  @view_class = view_class
43
102
  end
44
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
45
108
  def include_in_sitemap?
46
109
  true
47
110
  end
48
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
49
116
  def last_modified
50
117
  nil
51
118
  end
52
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
53
124
  def change_frequency
54
125
  nil
55
126
  end
56
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
57
132
  def priority
58
133
  nil
59
134
  end
@@ -4,9 +4,21 @@ require "concurrent/atomic/read_write_lock"
4
4
  require "listen"
5
5
 
6
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
7
17
  class ReloadCoordinator
18
+ # @return [Concurrent::ReadWriteLock] lock for coordinating reloads
8
19
  attr_reader :reload_lock
9
20
 
21
+ # Creates a new ReloadCoordinator and sets up file watchers.
10
22
  def initialize
11
23
  @reload_lock = Concurrent::ReadWriteLock.new
12
24
 
@@ -26,6 +38,13 @@ module Wytch
26
38
  end
27
39
  end
28
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]
29
48
  def reload!
30
49
  @start_site_code_listener&.call
31
50
  @start_content_listener&.call
data/lib/wytch/server.rb CHANGED
@@ -3,13 +3,38 @@
3
3
  require "rack"
4
4
 
5
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
6
19
  class Server
20
+ # @return [Hash] server options (port, host)
7
21
  attr_reader :options
8
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")
9
28
  def initialize(options = {})
10
29
  @options = options
11
30
  end
12
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]
13
38
  def start
14
39
  ENV["RACK_ENV"] = "development"
15
40
  Site.load!
@@ -30,6 +55,9 @@ module Wytch
30
55
 
31
56
  private
32
57
 
58
+ # Builds the Rack application stack.
59
+ #
60
+ # @return [Rack::Builder] the configured Rack app
33
61
  def app
34
62
  base_app = lambda { |env|
35
63
  path = env["PATH_INFO"]
data/lib/wytch/site.rb CHANGED
@@ -1,7 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wytch
4
+ # Holds the configuration and state for a Wytch site.
5
+ #
6
+ # The Site class is the central configuration point for Wytch. It manages
7
+ # the content directory, site code loading, page class, and the collection
8
+ # of all pages.
9
+ #
10
+ # @example Configuring a site in config.rb
11
+ # Wytch.configure do |site|
12
+ # site.page_class = "MySite::Page"
13
+ # site.base_url = "https://example.com"
14
+ # site.content_dir = "pages" # default is "content"
15
+ # end
16
+ #
17
+ # @see Wytch.configure
4
18
  class Site
19
+ # Loads the site configuration from config.rb in the current directory.
20
+ #
21
+ # @return [void]
5
22
  def self.load!
6
23
  if File.exist?(config_file = File.join(Dir.pwd, "config.rb"))
7
24
  load config_file
@@ -10,6 +27,7 @@ module Wytch
10
27
  end
11
28
  end
12
29
 
30
+ # Creates a new Site with default configuration.
13
31
  def initialize
14
32
  @inflections = {}
15
33
  @content_dir = "content"
@@ -19,13 +37,38 @@ module Wytch
19
37
  @base_url = nil
20
38
  end
21
39
 
40
+ # @return [Hash] custom inflections for Zeitwerk autoloading
22
41
  attr_reader :inflections
42
+
43
+ # @!attribute content_dir
44
+ # @return [String] directory containing content files (default: "content")
45
+ # @!attribute site_code_path
46
+ # @return [String] directory containing site Ruby code (default: "src")
47
+ # @!attribute page_class
48
+ # @return [String] fully qualified class name for pages (default: "Wytch::Page")
49
+ # @!attribute pages
50
+ # @return [Hash{String => Page}] all loaded pages, keyed by path
51
+ # @!attribute base_url
52
+ # @return [String, nil] base URL for the site (used in sitemaps, feeds, etc.)
23
53
  attr_accessor :content_dir, :site_code_path, :page_class, :pages, :base_url
24
54
 
55
+ # Adds custom inflections for Zeitwerk autoloading.
56
+ #
57
+ # @param inflections_hash [Hash{String => String}] mapping of file names to class names
58
+ # @return [void]
59
+ #
60
+ # @example
61
+ # site.inflect("api" => "API", "html_parser" => "HTMLParser")
25
62
  def inflect(inflections_hash)
26
63
  @inflections.merge!(inflections_hash)
27
64
  end
28
65
 
66
+ # Returns the Zeitwerk loader for site code.
67
+ #
68
+ # The loader is configured to watch the site_code_path directory and
69
+ # supports hot reloading during development.
70
+ #
71
+ # @return [Zeitwerk::Loader] the configured loader
29
72
  def site_code_loader
30
73
  @site_code_loader ||= begin
31
74
  loader = Zeitwerk::Loader.new
@@ -37,10 +80,16 @@ module Wytch
37
80
  end
38
81
  end
39
82
 
83
+ # Returns the content loader instance.
84
+ #
85
+ # @return [ContentLoader] the loader for content files
40
86
  def content_loader
41
87
  @content_loader ||= ContentLoader.new
42
88
  end
43
89
 
90
+ # Loads all content files and populates the pages hash.
91
+ #
92
+ # @return [Hash{String => Page}] all loaded pages
44
93
  def load_content
45
94
  @pages = content_loader.load_content
46
95
  end
@@ -1,12 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wytch
4
+ # Rack middleware that triggers hot reloading before each request.
5
+ #
6
+ # This middleware wraps the base application and coordinates with the
7
+ # ReloadCoordinator to ensure site code and content are reloaded when
8
+ # files change. It acquires a read lock during request processing to
9
+ # prevent reloads from interfering with rendering.
10
+ #
11
+ # @see Wytch::ReloadCoordinator
4
12
  class SiteCodeLoaderMiddleware
13
+ # Creates a new middleware instance.
14
+ #
15
+ # @param app [#call] the Rack application to wrap
16
+ # @param coordinator [ReloadCoordinator] the reload coordinator
5
17
  def initialize(app, coordinator)
6
18
  @app = app
7
19
  @coordinator = coordinator
8
20
  end
9
21
 
22
+ # Handles a Rack request.
23
+ #
24
+ # Triggers a reload check, then processes the request while holding
25
+ # a read lock to prevent concurrent reloads.
26
+ #
27
+ # @param env [Hash] the Rack environment
28
+ # @return [Array] the Rack response tuple
10
29
  def call(env)
11
30
  @coordinator.reload!
12
31
 
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wytch
4
+ # Helper module for sitemap pages.
5
+ #
6
+ # This module provides methods to configure a page as a sitemap.
7
+ # It sets the correct path, build path, and excludes the sitemap
8
+ # itself from appearing in the sitemap.
9
+ #
10
+ # @example Using in a content file (content/sitemap.rb)
11
+ # view Wytch::SitemapView
12
+ # add Wytch::SitemapHelper
13
+ module SitemapHelper
14
+ # Returns the URL path for the sitemap.
15
+ #
16
+ # @return [String] "/sitemap.xml"
17
+ def path
18
+ "/sitemap.xml"
19
+ end
20
+
21
+ # Returns the build path for the sitemap.
22
+ #
23
+ # @return [String] "sitemap.xml"
24
+ def build_path
25
+ "sitemap.xml"
26
+ end
27
+
28
+ # Excludes the sitemap from appearing in the sitemap.
29
+ #
30
+ # @return [Boolean] false
31
+ def include_in_sitemap?
32
+ false
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wytch
4
+ # Generates an XML sitemap for the site.
5
+ #
6
+ # This view renders a sitemap.xml file listing all pages that have
7
+ # `include_in_sitemap?` returning true.
8
+ #
9
+ # @example Using in a content file (content/sitemap.rb)
10
+ # view Wytch::SitemapView
11
+ # add Wytch::SitemapHelper
12
+ class SitemapView
13
+ # Creates a new sitemap view.
14
+ #
15
+ # @param page [Page] the page object (not used but required for interface)
16
+ def initialize(page)
17
+ @page = page
18
+ end
19
+
20
+ # Renders the sitemap XML.
21
+ #
22
+ # @return [String] XML sitemap content
23
+ def call
24
+ require "builder"
25
+
26
+ xml = ::Builder::XmlMarkup.new(indent: 2)
27
+ xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
28
+ xml.urlset xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9" do
29
+ Wytch.site.pages.values.each do |page|
30
+ next unless page.include_in_sitemap?
31
+ xml.url do
32
+ xml.loc "#{Wytch.site.base_url}#{page.path}"
33
+ xml.lastmod page.last_modified if page.last_modified
34
+ xml.changefreq page.change_frequency if page.change_frequency
35
+ xml.priority page.priority if page.priority
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- add <%= @site_module %>::SitemapHelper
4
- view <%= @site_module %>::SitemapView
3
+ add Wytch::SitemapHelper
4
+ view Wytch::SitemapView
@@ -11,6 +11,7 @@ module <%= @site_module %>
11
11
 
12
12
  html do
13
13
  head do
14
+ meta charset: "utf-8"
14
15
  title { @page.metadata[:title] }
15
16
  meta name: "description", content: @page.metadata[:description] if @page.metadata[:description]
16
17
  meta name: "viewport", content: "width=device-width, initial-scale=1.0"