sitepress-rails 5.0.0.beta2 → 5.0.0.beta3

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: d647985fa6763a9361d1af2906f6a1c22cc1cfd4e0e9cb010c010d088ab143eb
4
- data.tar.gz: facf3d69378e4abb6c8d32aa8b27e68ee413105df011990584df7241b9624c18
3
+ metadata.gz: 443c3d5ccdfa98e1b995ae899d3d8e20dd7ffb4c98a8d5700be29384847e838d
4
+ data.tar.gz: a7664658ced9775dbbde217f0247933851edbbbebb6c2cd5ca8d1e893163f2fd
5
5
  SHA512:
6
- metadata.gz: fa6aa939414cc4ddf067c01153b8488e7d224d5dcf0af9831f071e407f5e71de19f57770b48144fc491aec2ecb2a40d4efa0d0887bb10f80f28f282f86446331
7
- data.tar.gz: a0f9a313a88e42902d0388196dc9541368d1e88fcc6fffb0fb92e8df4d56dbc707f465d7578fecb5dc1847527bd41766d67af0c9b1b302c30ab8a7ce02a899c9
6
+ metadata.gz: e092de737d65d1e5eca7eefedcdf1f1129af9872e1fdca7418c5bf0b9192fe9fd71c885698779fe120f958c1e525f734a1fd919b3e457646354bbc3eb78e8e25
7
+ data.tar.gz: 3eab694d5c52e70410f9f2dde9421e31f5a329aed6f83bd5d447df29df2b9b870cef72a128627810fc7552277f071d88b59aae3b0803a9436826ced73e16fab0
data/README.md CHANGED
@@ -25,6 +25,77 @@ sitepress_root # Delete if you don't want your app's root page to be a content p
25
25
 
26
26
  Restart the Rails application server and point your browser to `http://127.0.0.1:3000/` and if all went well you should see a sitepress page.
27
27
 
28
+ ## Multiple sites in a single Rails app
29
+
30
+ You can serve any number of Sitepress sites from one Rails app — for example a marketing site at `/` and an admin docs site at `/admin/docs`. Three pieces, plain Ruby:
31
+
32
+ ```ruby
33
+ # 1. config/initializers/sitepress.rb — register the site at boot
34
+ Sitepress.sites << Sitepress::Site.new(root_path: "app/sitepress/admin_docs")
35
+ ```
36
+
37
+ ```ruby
38
+ # 2. app/controllers/admin/docs_controller.rb — bind it to a controller
39
+ class Admin::DocsController < Sitepress::SiteController
40
+ self.site = Sitepress.sites.fetch("app/sitepress/admin_docs")
41
+
42
+ layout "admin"
43
+ before_action :require_admin
44
+ end
45
+ ```
46
+
47
+ ```ruby
48
+ # 3. config/routes.rb — mount the controller
49
+ Rails.application.routes.draw do
50
+ sitepress_pages # default site at /
51
+
52
+ namespace :admin do
53
+ scope :docs do
54
+ sitepress_pages controller: "admin/docs", as: :admin_doc
55
+ end
56
+ end
57
+ end
58
+ ```
59
+
60
+ The whole multi-site API is two methods on `Sitepress`: `Sitepress.site` (the configured default, unchanged) and `Sitepress.sites` (the registry). The registry has three operations — `<<` to add, `fetch` to look up by `root_path` (raises `NotFoundError` listing registered paths on miss), and `each` plus the rest of `Enumerable` for iteration.
61
+
62
+ A typo in the path string fails loud at controller class load:
63
+
64
+ ```
65
+ NotFoundError: No Sitepress site registered at "app/contnet".
66
+ Registered: ["app/sitepress/admin_docs"]
67
+ ```
68
+
69
+ **Why three pieces and not one?** Boot ordering forces it. Zeitwerk needs helper / model paths registered before its eager-load pass, which happens before the first request — that's what `Sitepress.sites <<` does and it's the only piece that *has* to live in an initializer. The controller binding (`self.site = ...`) is just `class_attribute` plus a writer that `prepend_view_path`s the site's view directories onto *this controller's* lookup chain (so multi-site view lookups stay local — no global ActionView pollution). Routes own the URL → controller binding, with the mount path read from the surrounding `scope`/`namespace`.
70
+
71
+ The same site can be referenced by more than one controller — a public reader and an admin editor can both `Sitepress.sites.fetch("...")` and bind to the same content tree. The Site itself is registered once.
72
+
73
+ ### Generator
74
+
75
+ ```bash
76
+ bin/rails generate sitepress:site app/sitepress/admin_docs
77
+ ```
78
+
79
+ Scaffolds the content directory tree (`pages/`, `helpers/`, `models/`, `assets/`), a stub index template, a controller subclass with `self.site = Sitepress.sites.fetch(...)` already filled in, and either creates or appends to `config/initializers/sitepress.rb` with the registration line. Pass `--mount-at=/admin/docs` to also inject a `scope` block into `config/routes.rb`; without the flag, the generator just prints the routes line for you to paste.
80
+
81
+ ### Rake tasks
82
+
83
+ Compilation is split into single-site and multi-site forms so single-site users don't get a behavior change when they add a registered site:
84
+
85
+ - `rake sitepress:compile` — compiles the configured default site only.
86
+ - `rake sitepress:sites:compile` — compiles every registered site (default + `Sitepress.sites`) to `tmp/sitepress/<basename>`. Each site lives in its own subdirectory so two sites never collide on output.
87
+ - `rake "sitepress:sites:compile[app/sitepress/admin_docs]"` — compiles a single registered site by `root_path`. Raises `Sitepress::NotFoundError` listing registered paths if no match.
88
+ - `rake sitepress:sites` — lists the configured default site and everything in `Sitepress.sites`.
89
+
90
+ Two env vars adjust the compile tasks:
91
+
92
+ - `OUTPUT_PATH=build rake sitepress:sites:compile` — overrides the default `tmp/sitepress` build root.
93
+ - `FAIL_ON_ERROR=true rake sitepress:sites:compile` — raises on the first resource that fails to render. Default (`false`) collects all failures and prints a summary at the end.
94
+
95
+ After every compile run, the tasks print a `Compilation Summary` block listing how many sites were built, how many resources succeeded/failed, and (if any failed) the path of every failing resource paired with the site it lives in.
96
+
97
+ These tasks only handle content compilation. Run your asset bundler (Propshaft, esbuild, Tailwind, etc.) separately if you have static assets to build.
98
+
28
99
  ## License
29
100
 
30
101
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,57 @@
1
+ module Sitepress
2
+ # A dumb collection of compiler instances. Holds anything that
3
+ # responds to `#compile` (typically `Sitepress::Compiler::Files`),
4
+ # and runs them in order when you call `#compile`. The collection
5
+ # has no opinion about how its members were constructed, where they
6
+ # write, or what sites they're bound to — that's all the caller's
7
+ # job.
8
+ #
9
+ # compilers = Sitepress::Compilers.new
10
+ # compilers << Sitepress::Compiler::Files.new(site: foo, root_path: "build/foo")
11
+ # compilers << Sitepress::Compiler::Files.new(site: bar, root_path: "build/bar")
12
+ # compilers.compile
13
+ #
14
+ # `Enumerable` is mixed in, so `compilers.flat_map(&:succeeded)`
15
+ # and `compilers.flat_map(&:failed)` work for aggregating across
16
+ # the underlying compilers.
17
+ class Compilers
18
+ include Enumerable
19
+
20
+ def initialize(compilers = [])
21
+ @compilers = compilers.to_a
22
+ end
23
+
24
+ # Add a single compiler to the collection. Returns self per Ruby's
25
+ # `<<` convention so additions chain.
26
+ def <<(compiler)
27
+ @compilers << compiler
28
+ self
29
+ end
30
+
31
+ # Merge an iterable of compilers into the collection. Mirrors
32
+ # `Array#concat` — accepts anything responding to `#to_a` (Array,
33
+ # another Compilers, lazy enumerator, etc.) and adds each element
34
+ # individually.
35
+ #
36
+ # compilers.concat([c1, c2, c3])
37
+ # compilers.concat(other_compilers)
38
+ # compilers.concat(sites.map { |s| Compiler::Files.new(...) })
39
+ #
40
+ # Returns self for chaining.
41
+ def concat(other)
42
+ @compilers.concat(other.to_a)
43
+ self
44
+ end
45
+
46
+ # Run `#compile` on every compiler in the collection in order.
47
+ # Returns self for chaining.
48
+ def compile
49
+ @compilers.each(&:compile)
50
+ self
51
+ end
52
+
53
+ def each(&block)
54
+ @compilers.each(&block)
55
+ end
56
+ end
57
+ end
@@ -24,22 +24,27 @@ module Sitepress
24
24
  "app/markdown" # When Sitepress is launched embedded in Rails project.
25
25
  ], autoload: true
26
26
 
27
- # Load paths from `Sitepress#site` into Rails.
27
+ # Load paths from Sitepress sites into Rails.
28
28
  #
29
- # We configure two separate systems:
29
+ # The work is split across two initializers because of Rails' boot
30
+ # ordering: the *default* site is set up before `:set_autoload_paths`
31
+ # (so its paths land in `config.autoload_paths` the normal way),
32
+ # but *registered* sites have to wait until after
33
+ # `:load_config_initializers` runs — that's where the user's
34
+ # `config/initializers/sitepress.rb` (with `Sitepress.sites << ...`)
35
+ # populates the registry. By the time we iterate the registry,
36
+ # `:set_autoload_paths` has already frozen `config.autoload_paths`,
37
+ # so we push paths directly to `Rails.autoloaders.main` (which
38
+ # accepts additions any time before eager loading).
30
39
  #
31
- # 1. app.paths["app/*"] - Rails component path registry
32
- # Tells ActionView where to find templates, ActionController where to find helpers, etc.
33
- #
34
- # 2. config.autoload_paths - Zeitwerk autoloader configuration
35
- # Tells Zeitwerk what to autoload. Rails automatically includes these in
36
- # config.eager_load_paths for production environments.
37
- #
38
- initializer "sitepress.set_paths", before: :set_autoload_paths do |app|
40
+ # The default site gets views added to `app/views` globally; the
41
+ # default `Sitepress::SiteController` reads from there. Registered
42
+ # sites' views live on the controller via `prepend_view_path` so
43
+ # multi-site view lookups stay local to the controller.
44
+
45
+ initializer "sitepress.set_default_site_paths", before: :set_autoload_paths do |app|
39
46
  site = Sitepress.configuration.site
40
47
 
41
- # Helpers: autoloadable and available to controllers
42
- # Collapsed so app/content/helpers/sample_helper.rb defines SampleHelper (not Helpers::SampleHelper)
43
48
  site.helpers_path.expand_path.tap do |path|
44
49
  if path.exist?
45
50
  app.paths["app/helpers"].push path
@@ -50,8 +55,6 @@ module Sitepress
50
55
  end
51
56
  end
52
57
 
53
- # Models: autoloadable
54
- # Collapsed so models don't require namespace prefixes
55
58
  site.models_path.expand_path.tap do |path|
56
59
  if path.exist?
57
60
  app.paths["app/models"].push path
@@ -62,21 +65,43 @@ module Sitepress
62
65
  end
63
66
  end
64
67
 
65
- # Assets: available to Sprockets (no autoloading needed)
66
68
  app.paths["app/assets"].push site.assets_path.expand_path
69
+ app.paths["app/views"].push site.root_path.expand_path
70
+ app.paths["app/views"].push site.pages_path.expand_path
71
+ end
67
72
 
68
- # Views: available to ActionView (no autoloading needed - these are templates)
69
- app.paths["app/views"].push site.root_path.expand_path
70
- app.paths["app/views"].push site.pages_path.expand_path
73
+ initializer "sitepress.set_registered_site_paths", after: :load_config_initializers do |app|
74
+ # `config.autoload_paths` and `config.eager_load_paths` are frozen
75
+ # by the time `:load_config_initializers` finishes, but
76
+ # `Rails.autoloaders.main.push_dir` accepts new directories any
77
+ # time before eager loading. Registered sites' helpers/models
78
+ # become lazy-autoloadable through that path. They're not added
79
+ # to `eager_load_paths`, so in production they're loaded on first
80
+ # access rather than at boot — fine for the multi-site case
81
+ # where the secondary site's helpers are scoped to one controller.
82
+ register_helpers_late = ->(path) {
83
+ if path.exist?
84
+ Rails.autoloaders.main.push_dir(path)
85
+ Rails.autoloaders.main.collapse(path)
86
+ end
87
+ }
71
88
 
72
- # Components: autoloadable for view_components
73
- app.config.autoload_paths << File.expand_path("./components")
74
- end
89
+ register_assets_late = ->(path) {
90
+ # Propshaft reads config.assets.paths lazily, so adding here
91
+ # works for both dev and production precompile.
92
+ app.config.assets.paths << path.to_s if app.config.respond_to?(:assets)
93
+ }
94
+
95
+ Sitepress.configuration.sites.each do |site|
96
+ register_helpers_late.call site.helpers_path.expand_path
97
+ register_helpers_late.call site.models_path.expand_path
98
+ register_assets_late.call site.assets_path.expand_path
99
+ end
75
100
 
76
- # Configure sprockets paths for the site.
77
- initializer "sitepress.set_manifest_file_path", before: :append_assets_path do |app|
78
- manifest_file = Sitepress.configuration.manifest_file_path.expand_path
79
- app.config.assets.precompile << manifest_file.to_s if manifest_file.exist?
101
+ # Mark that boot-time path registration has finished. Sites#<<
102
+ # checks this and warns if a site is registered after this point,
103
+ # since its helpers/models/assets won't be picked up by Zeitwerk.
104
+ Sitepress.configuration.instance_variable_set(:@boot_paths_registered, true)
80
105
  end
81
106
 
82
107
  # Configure Sitepress with Rails settings.
@@ -2,11 +2,13 @@ require "sitepress-core"
2
2
 
3
3
  module Sitepress
4
4
  autoload :Compiler, "sitepress/compiler"
5
+ autoload :Compilers, "sitepress/compilers"
5
6
  autoload :Model, "sitepress/model"
6
7
  module Models
7
8
  autoload :Collection, "sitepress/models/collection"
8
9
  end
9
10
  autoload :RailsConfiguration, "sitepress/rails_configuration"
11
+ autoload :Sites, "sitepress/sites"
10
12
  module Renderers
11
13
  autoload :Controller, "sitepress/renderers/controller"
12
14
  autoload :Server, "sitepress/renderers/server"
@@ -30,11 +32,24 @@ module Sitepress
30
32
  # Raised when any of the Render subclasses can't render a page.
31
33
  RenderingError = Class.new(RuntimeError)
32
34
 
33
- # Make site available via Sitepress.site from Rails app.
35
+ # The configured default site (single-site case). For multi-site
36
+ # apps, additional sites live in `Sitepress.sites`.
34
37
  def self.site
35
38
  configuration.site
36
39
  end
37
40
 
41
+ # Registry of additional `Sitepress::Site` instances for multi-site
42
+ # apps. See `Sitepress::Sites` for the full API; the common usage is:
43
+ #
44
+ # # config/initializers/sitepress.rb
45
+ # Sitepress.sites << Sitepress::Site.new(root_path: "app/sitepress/admin_docs")
46
+ #
47
+ # # somewhere later (e.g. a controller class body)
48
+ # Sitepress.sites.fetch("app/sitepress/admin_docs")
49
+ def self.sites
50
+ configuration.sites
51
+ end
52
+
38
53
  # Default configuration object for Sitepress Rails integration.
39
54
  def self.configuration
40
55
  @configuration ||= RailsConfiguration.new
@@ -14,6 +14,14 @@ module Sitepress
14
14
  self.cache_resources = true
15
15
  end
16
16
 
17
+ # Registry of additional `Sitepress::Site` instances for multi-site
18
+ # apps. Lives on the configuration object so its lifecycle is tied
19
+ # to the Rails app and tests can reset it via
20
+ # `Sitepress.reset_configuration`.
21
+ def sites
22
+ @sites ||= Sites.new
23
+ end
24
+
17
25
  def parent_engine
18
26
  @parent_engine ||= Rails.application
19
27
  end
@@ -22,11 +30,6 @@ module Sitepress
22
30
  @site ||= pending_site || Site.new(root_path: default_root)
23
31
  end
24
32
 
25
- # Location of Sprockets manifest file
26
- def manifest_file_path
27
- site.assets_path.join("config/manifest.js")
28
- end
29
-
30
33
  private
31
34
 
32
35
  def pending_site
@@ -1,14 +1,58 @@
1
1
  module Sitepress
2
- # Route constraint for rails routes.rb file.
2
+ # Route constraint for the Rails routes file. Two ways to construct it:
3
+ #
4
+ # - Pass `site:` directly — used by the default `sitepress_pages` route
5
+ # for the single-site case.
6
+ #
7
+ # - Pass `controller:` (a string like `"admin/docs"`) — the controller
8
+ # class is resolved lazily and its class-level `.site` method is called
9
+ # per request. This is what lets a controller bind itself to a site
10
+ # via `class_attribute :site` and have routing pick it up:
11
+ #
12
+ # class Admin::DocsController < Sitepress::SiteController
13
+ # self.site = Sitepress.sites.fetch("app/sitepress/admin_docs")
14
+ # end
15
+ #
16
+ # `path_prefix` is the URL prefix the route is mounted under (e.g.
17
+ # `/admin/docs`), stripped from `request.path` before the resource lookup.
18
+ # `sitepress_pages` fills this in from `@scope[:path]` automatically.
3
19
  class RouteConstraint
4
- attr_reader :site
20
+ attr_reader :path_prefix
5
21
 
6
- def initialize(site: Sitepress.site)
7
- @site = site
22
+ def initialize(site: nil, controller: nil, path_prefix: nil)
23
+ @explicit_site = site
24
+ @controller_name = controller
25
+ @path_prefix = path_prefix.presence
8
26
  end
9
27
 
10
28
  def matches?(request)
11
- !!site.resources.get(request.path)
29
+ !!site.resources.get(resource_path(request))
30
+ end
31
+
32
+ # Resolves the site this constraint guards. If a site was passed in
33
+ # explicitly we use it; otherwise we resolve the controller class and
34
+ # ask it. Resolution is lazy because controllers may not be loaded
35
+ # when routes are drawn.
36
+ def site
37
+ @explicit_site || controller_class.site
38
+ end
39
+
40
+ private
41
+
42
+ def controller_class
43
+ @controller_class ||= begin
44
+ raise ArgumentError, "Sitepress::RouteConstraint needs site: or controller:" if @controller_name.nil?
45
+ "#{@controller_name}_controller".camelize.constantize
46
+ end
47
+ end
48
+
49
+ def resource_path(request)
50
+ path = request.path
51
+ if path_prefix && path.start_with?(path_prefix)
52
+ path = path.delete_prefix(path_prefix)
53
+ path = "/" if path.empty?
54
+ end
55
+ path
12
56
  end
13
57
  end
14
58
  end
@@ -7,8 +7,32 @@ module ActionDispatch::Routing
7
7
  DEFAULT_ACTION = "show".freeze
8
8
  ROUTE_GLOB_KEY = "/*resource_path".freeze
9
9
 
10
- # Hook up all the Sitepress pages
11
- def sitepress_pages(controller: DEFAULT_CONTROLLER, action: DEFAULT_ACTION, root: false, constraints: Sitepress::RouteConstraint.new, as: :page)
10
+ # Hook up all the Sitepress pages.
11
+ #
12
+ # The mount path is read from the surrounding Rails `scope`/`namespace`,
13
+ # so it never has to be repeated. The site is read from the controller
14
+ # class's `.site` method, so a multi-site app declares the site once on
15
+ # the controller and the routes file just mounts it.
16
+ #
17
+ # @example Default site at the root
18
+ # sitepress_pages
19
+ #
20
+ # @example A controller-owned site mounted under /admin/docs
21
+ # # app/controllers/admin/docs_controller.rb
22
+ # class Admin::DocsController < Sitepress::SiteController
23
+ # def self.site = Sitepress.site(:admin_docs)
24
+ # end
25
+ #
26
+ # # config/routes.rb
27
+ # namespace :admin do
28
+ # scope :docs do
29
+ # sitepress_pages controller: "admin/docs"
30
+ # end
31
+ # end
32
+ def sitepress_pages(controller: DEFAULT_CONTROLLER, action: DEFAULT_ACTION, root: false, constraints: nil, as: :page)
33
+ path_prefix = @scope[:path].presence
34
+ constraints ||= Sitepress::RouteConstraint.new(controller: controller, path_prefix: path_prefix)
35
+
12
36
  get ROUTE_GLOB_KEY,
13
37
  controller: controller,
14
38
  action: action,
@@ -0,0 +1,88 @@
1
+ module Sitepress
2
+ # Registry of `Sitepress::Site` instances for multi-site Rails apps.
3
+ #
4
+ # The whole multi-site API lives on this collection — `<<` to register,
5
+ # `fetch` to look up by `root_path`, `each` (and the rest of `Enumerable`)
6
+ # to iterate. There's intentionally no `[]`, no `add`, no `find`-by-block,
7
+ # and no `delete` — three methods plus Enumerable is the entire surface,
8
+ # so callers don't have to choose between soft-miss and hard-miss lookup
9
+ # forms or between `<<` and a non-chaining `add`.
10
+ #
11
+ # # config/initializers/sitepress.rb
12
+ # Sitepress.sites << Sitepress::Site.new(root_path: "app/sitepress/admin_docs")
13
+ #
14
+ # # somewhere later
15
+ # Sitepress.sites.fetch("app/sitepress/admin_docs") # => Sitepress::Site
16
+ # Sitepress.sites.fetch("nope") # => raises NotFoundError
17
+ # Sitepress.sites.each { |site| ... }
18
+ #
19
+ # The collection holds Sites directly — there's no derived key stored
20
+ # alongside, so the "key drift" failure mode (registry key disagreeing
21
+ # with the value's actual root_path) is impossible by construction.
22
+ # Lookup is a linear scan over `Site#root_path`, which is fine because
23
+ # registration is rare (boot time) and lookups happen at controller class
24
+ # load (also boot, in production).
25
+ class Sites
26
+ include Enumerable
27
+
28
+ def initialize
29
+ @sites = []
30
+ end
31
+
32
+ # Register a Site. Returns self per Ruby's `<<` convention so
33
+ # `Sitepress.sites << a << b` chains correctly.
34
+ #
35
+ # Two safety checks:
36
+ #
37
+ # - Type-checks `site` so the common mistake of pushing a path
38
+ # string instead of a constructed Site fails loudly at the
39
+ # call site rather than later when the engine tries to read
40
+ # `.helpers_path` on a String.
41
+ #
42
+ # - If the engine's path-setup pass has already finished (i.e.
43
+ # `<<` is being called from `config.after_initialize`, a
44
+ # request, or anywhere else that runs after initializers), the
45
+ # site is registered but its helpers/models/assets won't be on
46
+ # Zeitwerk's autoload paths. We log a warning so the user can
47
+ # diagnose the silent half-broken state instead of debugging
48
+ # "why isn't my AdminDocsHelper resolving" three days later.
49
+ def <<(site)
50
+ unless site.is_a?(Sitepress::Site)
51
+ raise ArgumentError,
52
+ "Sitepress.sites << expects a Sitepress::Site, got #{site.class}: #{site.inspect}. " \
53
+ "Wrap it: Sitepress.sites << Sitepress::Site.new(root_path: #{site.inspect})"
54
+ end
55
+
56
+ if Sitepress.respond_to?(:configuration) &&
57
+ Sitepress.configuration.instance_variable_get(:@boot_paths_registered) &&
58
+ defined?(Rails) && Rails.logger
59
+ Rails.logger.warn(
60
+ "Sitepress.sites << #{site.root_path.inspect} called after the engine's " \
61
+ "path-setup pass. The site is registered, but its helpers, models, and " \
62
+ "assets won't be on Zeitwerk's autoload paths. Move this call into " \
63
+ "config/initializers/ if you want Rails to discover them."
64
+ )
65
+ end
66
+
67
+ @sites << site
68
+ self
69
+ end
70
+
71
+ # Find a registered Site by its `root_path`. Raises `NotFoundError`
72
+ # listing the registered paths if nothing matches — there's no nil
73
+ # return form, so a typo in the path string fails loud at the call
74
+ # site instead of propagating into a `NoMethodError on nil` later.
75
+ def fetch(root_path)
76
+ key = root_path.to_s
77
+ @sites.find { |site| site.root_path.to_s == key } || raise(
78
+ NotFoundError,
79
+ "No Sitepress site registered at #{root_path.inspect}. " \
80
+ "Registered: #{@sites.map { |s| s.root_path.to_s }.inspect}"
81
+ )
82
+ end
83
+
84
+ def each(&block)
85
+ @sites.each(&block)
86
+ end
87
+ end
88
+ end
@@ -1,4 +1,113 @@
1
- # desc "Explaining what the task does"
2
- # task :sitepress-rails do
3
- # # Task goes here
4
- # end
1
+ # OUTPUT_PATH overrides the default tmp/sitepress build root for both
2
+ # `sitepress:compile` and `sitepress:sites:compile`. CI and deploy
3
+ # flows that want a different location (`build/`, `dist/`,
4
+ # `public/static/`) set OUTPUT_PATH on the invocation:
5
+ #
6
+ # OUTPUT_PATH=build rake sitepress:sites:compile
7
+ SITEPRESS_COMPILE_OUTPUT_ROOT = -> {
8
+ ENV.fetch("OUTPUT_PATH") { Rails.root.join("tmp/sitepress").to_s }
9
+ }
10
+
11
+ # FAIL_ON_ERROR=true makes the compile rake tasks raise on the first
12
+ # resource that fails to render, aborting rake with a non-zero exit.
13
+ # Default (false) collects all failures and prints a summary at the
14
+ # end so a single bad page doesn't block the rest of the build —
15
+ # preferred for local iteration. Use FAIL_ON_ERROR=true in CI to make
16
+ # deploys notice broken pages.
17
+ SITEPRESS_FAIL_ON_ERROR = -> { ENV["FAIL_ON_ERROR"] == "true" }
18
+
19
+ # Build a Compiler::Files for `site`, writing into a subdirectory of
20
+ # `output_root` named after the site's basename. Used by both
21
+ # `sitepress:compile` and `sitepress:sites:compile` so they agree on
22
+ # the on-disk layout (each site in its own directory under the build
23
+ # root, no two sites colliding).
24
+ SITEPRESS_BUILD_COMPILER = ->(site, output_root, fail_on_error: false) {
25
+ Sitepress::Compiler::Files.new(
26
+ site: site,
27
+ root_path: Pathname(output_root).join(site.root_path.expand_path.basename.to_s),
28
+ fail_on_error: fail_on_error
29
+ )
30
+ }
31
+
32
+ # Print a "Compilation Summary" block to stdout after a compile run.
33
+ # Aggregates succeeded/failed counts across every compiler in the
34
+ # collection and lists each failed resource with its owning site so
35
+ # the user can see exactly what broke.
36
+ SITEPRESS_PRINT_COMPILE_SUMMARY = ->(compilers) {
37
+ succeeded = compilers.flat_map(&:succeeded).size
38
+ failed = compilers.flat_map(&:failed)
39
+
40
+ puts ""
41
+ puts "Compilation Summary"
42
+ puts " Sites: #{compilers.size}"
43
+ puts " Succeeded: #{succeeded}"
44
+ puts " Failed: #{failed.size}"
45
+
46
+ if failed.any?
47
+ puts ""
48
+ puts "Failed Resources"
49
+ compilers.each do |compiler|
50
+ compiler.failed.each do |resource|
51
+ puts " #{compiler.site.root_path}: #{resource.request_path}"
52
+ end
53
+ end
54
+ end
55
+ }
56
+
57
+ namespace :sitepress do
58
+ desc "Compile the configured default Sitepress site to OUTPUT_PATH (default: tmp/sitepress/<basename>)"
59
+ task compile: :environment do
60
+ require "sitepress/compiler"
61
+ output_root = SITEPRESS_COMPILE_OUTPUT_ROOT.call
62
+ fail_on_error = SITEPRESS_FAIL_ON_ERROR.call
63
+
64
+ compilers = Sitepress::Compilers.new
65
+ compilers << SITEPRESS_BUILD_COMPILER.call(Sitepress.site, output_root, fail_on_error: fail_on_error)
66
+ compilers.compile
67
+
68
+ SITEPRESS_PRINT_COMPILE_SUMMARY.call(compilers)
69
+ end
70
+
71
+ desc "List the configured default site and all sites in Sitepress.sites"
72
+ task sites: :environment do
73
+ puts "Default:"
74
+ puts " #{Sitepress.site.root_path}"
75
+
76
+ if Sitepress.sites.any?
77
+ puts ""
78
+ puts "Registered (#{Sitepress.sites.count}):"
79
+ Sitepress.sites.each do |site|
80
+ puts " - #{site.root_path}"
81
+ end
82
+ else
83
+ puts ""
84
+ puts "No additional sites registered. Use Sitepress.sites << Sitepress::Site.new(root_path: ...) in an initializer."
85
+ end
86
+ end
87
+
88
+ namespace :sites do
89
+ desc "Compile every registered Sitepress site (default + Sitepress.sites) to OUTPUT_PATH (default: tmp/sitepress/<basename>)"
90
+ task :compile, [:root_path] => :environment do |_task, args|
91
+ require "sitepress/compiler"
92
+ output_root = SITEPRESS_COMPILE_OUTPUT_ROOT.call
93
+ fail_on_error = SITEPRESS_FAIL_ON_ERROR.call
94
+
95
+ # When invoked as `rake sitepress:sites:compile` we compile
96
+ # everything; when invoked as `rake "sitepress:sites:compile[path]"`
97
+ # we compile just the matching site (raising NotFoundError with
98
+ # the registered paths listed if no match).
99
+ sites = if args[:root_path]
100
+ [Sitepress.sites.fetch(args[:root_path])]
101
+ else
102
+ [Sitepress.site, *Sitepress.sites]
103
+ end
104
+
105
+ compilers = Sitepress::Compilers.new.concat(
106
+ sites.map { |site| SITEPRESS_BUILD_COMPILER.call(site, output_root, fail_on_error: fail_on_error) }
107
+ )
108
+ compilers.compile
109
+
110
+ SITEPRESS_PRINT_COMPILE_SUMMARY.call(compilers)
111
+ end
112
+ end
113
+ end