jekyll-imgflow 0.1.9 → 0.2.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -13
  3. data/lib/jekyll-imgflow/batch_manager.rb +10 -7
  4. data/lib/jekyll-imgflow/build_time_processor.rb +70 -61
  5. data/lib/jekyll-imgflow/config.rb +3 -6
  6. data/lib/jekyll-imgflow/filename_generator.rb +8 -10
  7. data/lib/jekyll-imgflow/generated_file_cleaner.rb +81 -0
  8. data/lib/jekyll-imgflow/hooks.rb +13 -6
  9. data/lib/jekyll-imgflow/html_generator.rb +14 -19
  10. data/lib/jekyll-imgflow/imgflow_tag.rb +84 -69
  11. data/lib/jekyll-imgflow/manifest_manager.rb +132 -53
  12. data/lib/jekyll-imgflow/operation_processor.rb +43 -22
  13. data/lib/jekyll-imgflow/picture_tag_adaptor.rb +1 -1
  14. data/lib/jekyll-imgflow/preset_manager.rb +58 -7
  15. data/lib/jekyll-imgflow/presets/gallery.yml +7 -0
  16. data/lib/jekyll-imgflow/presets/hero.yml +7 -0
  17. data/lib/jekyll-imgflow/presets/thumbnail.yml +7 -0
  18. data/lib/jekyll-imgflow/processing_stats.rb +80 -0
  19. data/lib/jekyll-imgflow/provider_registry.rb +18 -3
  20. data/lib/jekyll-imgflow/providers/base_provider.rb +12 -1
  21. data/lib/jekyll-imgflow/providers/flyimg.rb +2 -1
  22. data/lib/jekyll-imgflow/providers/imagemagick.rb +72 -3
  23. data/lib/jekyll-imgflow/providers/imgproxy.rb +2 -1
  24. data/lib/jekyll-imgflow/providers/libvips.rb +152 -100
  25. data/lib/jekyll-imgflow/providers/sharp.rb +2 -1
  26. data/lib/jekyll-imgflow/providers/weserv.rb +3 -2
  27. data/lib/jekyll-imgflow/tags/opacity_tag.rb +8 -1
  28. data/lib/jekyll-imgflow/tags/resize_tag.rb +2 -0
  29. data/lib/jekyll-imgflow/tags/watermark_tag.rb +8 -1
  30. data/lib/jekyll-imgflow/tasks.rb +129 -0
  31. data/lib/jekyll-imgflow/version.rb +1 -1
  32. data/lib/jekyll-imgflow.rb +2 -0
  33. metadata +24 -156
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 031d0986b4c5e0b451cc3d404eb7bc251e5ee61791fa19a9ae71ad5dc2a2e46b
4
- data.tar.gz: 658faf7858443964c239ebbab8a343e51430c1c5043cf6e413c3c2463d4b68a8
3
+ metadata.gz: 3b95efe6605d712e14895c4152a68b4b751eea6954416e428acbe96518bd4284
4
+ data.tar.gz: 434fc1985138599142a94d32dda9c986c63893c4acfc587e085d8e3486d700d4
5
5
  SHA512:
6
- metadata.gz: 349afebeceaea49781508b0156177cc07eb1cf0d440ef3fe9754cfec279428b76d9ca2716b991744d66523c16a42102c8a31055864612bcb921b313a69d0c900
7
- data.tar.gz: aef1c22691523d287e2aebb7c4e12f8247f7e2cb9b9999ee0368373141c3eddb88c02c96198bf97364a8e04565d09063f447c33d2cdc89af3b5826f40bedcc15
6
+ metadata.gz: f588617884137bcf8054e2a1bce6489b9aebfe4829ce9a279bb65654871dd13d41df6e6f0e0a15ddeb7fb39b9f4f9776206a2f6ff16b83db6e11c82b76dd78ef
7
+ data.tar.gz: 480ba9208f2952793bff054b24b8f7e79a47de1b83d322d0f0f261242cbd8d01f4e532b0571397d4f0efb18c31f27867546ba987f51add3a664fb5b7049bcfb1
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # ImgFlow
2
2
 
3
+ ![Jekyll ImgFlow](docs/assets/images/jekyll-imgflow-logo.png)
4
+
3
5
  A [Jekyll](https://jekyllrb.com/) plugin for automatic image optimization with multiple providers and formats.
4
6
 
5
7
  [![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/gundestrup/jekyll-imgflow)
@@ -27,10 +29,9 @@ imgflow:
27
29
  {% imgflow photo.jpg resize width:800 %}
28
30
  ```
29
31
 
30
- > **Full configuration options:** `quality`, `backend_priority`, `formats`,
31
- > `fallback_format`, `sizes`, provider URLs, and more see the
32
- > [Installation Guide](docs/installation.md#4-add-to-_configyml)
33
- > and [Configuration Reference](docs/ARCHITECTURE.md).
32
+ > **Configuration:** Only `originals` and `output` are required in `_config.yml`
33
+ > everything else has sensible defaults. See the
34
+ > [Installation Guide](docs/installation.md#4-add-to-_configyml) for full options.
34
35
 
35
36
  > **Note:** Image references require exact filenames (or paths relative to
36
37
  > `originals`). There is no fuzzy matching during the build yet — if you type
@@ -84,12 +85,12 @@ ImgFlow has two processing flows: **Build-Time** (pre-generation) and **Runtime*
84
85
  ## 🎯 Provider Support
85
86
 
86
87
  ### HTTP API Services (Recommended)
87
- - **[Imgproxy](https://github.com/imgproxy/imgproxy)** - Fast, reliable (Port 33001)
88
- - **[Weserv](https://github.com/weserv/images)** - Battle-tested (Port 33007)
89
- - **[Flyimg](https://github.com/flyimg/flyimg)** - On-the-fly processing (Port 33008)
88
+ - **[Imgproxy](https://github.com/imgproxy/imgproxy)** - Fast, reliable (Port 4022)
89
+ - **[Weserv](https://github.com/weserv/images)** - Battle-tested (Port 4026)
90
+ - **[Flyimg](https://github.com/flyimg/flyimg)** - On-the-fly processing (Port 4030)
90
91
 
91
92
  ### CLI Tools (Local)
92
- - **[Sharp](https://github.com/lovell/sharp)** - Fastest (Node.js/libvips)
93
+ - **[Sharp](https://github.com/lovell/sharp)** - Node.js/libvips processing
93
94
  - **[ImageMagick](https://github.com/ImageMagick/ImageMagick)** - Feature-rich
94
95
  - **[LibVips](https://github.com/libvips/libvips)** - Memory efficient
95
96
 
@@ -108,7 +109,7 @@ rake test # All tests
108
109
  rake parallel:test # Parallel testing (faster)
109
110
 
110
111
  # Docker Services
111
- docker-compose -f docker-compose.test.yml up -d # Start services
112
+ rake start_services # Start services (pulls latest pinned images)
112
113
  rake check_services # Verify services
113
114
  ```
114
115
 
@@ -128,25 +129,35 @@ bundle install
128
129
  imgflow:
129
130
  originals: "assets/images/originals"
130
131
  output: "assets/images/optimized"
132
+ # cache_dir defaults to ".cache/imgflow" (stores manifest, survives _site wipes)
131
133
 
132
134
  # Use in templates
133
135
  {% imgflow photo.jpg resize width:800 %}
136
+
137
+ # Or use built-in presets:
138
+ {% imgflow photo.jpg preset:gallery %}
134
139
  ```
135
140
 
141
+ **Built-in presets:** `gallery` (400px, avif/webp/jpg, Q80), `hero` (800px, avif/webp/jpg, Q85), `thumbnail` (150px, webp/jpg, Q75). They work without installation. To copy editable versions, add `require "jekyll-imgflow/tasks"` to the site's `Rakefile`, then run `rake imgflow:install_presets`.
142
+
136
143
  **See:** [installation.md](docs/installation.md) for detailed installation and
137
- [all configuration options](docs/installation.md#4-add-to-_configyml)
144
+ [all configuration options](docs/installation.md#4-add-to-_configyml),
145
+ [presets.md](docs/usage/presets.md) for the preset system
138
146
 
139
147
  ## 🐳 Docker Setup (Recommended)
140
148
 
141
149
  ```bash
142
- # Start all services
143
- docker-compose -f docker-compose.test.yml --env-file .env.test up -d
150
+ # Start all services (pulls latest pinned images first)
151
+ rake start_services
144
152
 
145
153
  # Check services
146
154
  rake check_services
147
155
 
156
+ # Check if pinned images are outdated (run before releases)
157
+ rake check_docker_images
158
+
148
159
  # Stop services
149
- docker-compose -f docker-compose.test.yml down
160
+ rake stop_services
150
161
  ```
151
162
 
152
163
  **See:** [docker.md](docs/docker.md) for detailed Docker configuration and services
@@ -91,11 +91,12 @@ module JekyllImgFlow
91
91
  def process_task(task)
92
92
  # Check if processing is needed
93
93
  # Check if output is up-to-date
94
- if task[:skip_if_exists] && File.exist?(task[:output_path]) && !@operation_processor.needs_processing?(
95
- task[:input_path],
96
- task[:output_path],
97
- task[:params]
98
- )
94
+ if !task[:force_processing] && task[:skip_if_exists] && File.exist?(task[:output_path]) &&
95
+ !@operation_processor.needs_processing?(
96
+ task[:input_path],
97
+ task[:output_path],
98
+ task[:params]
99
+ )
99
100
  Jekyll.logger.debug "⏭️ Skipping #{task[:original_name]} - already up-to-date"
100
101
  mark_completed(task, :skipped)
101
102
  return
@@ -104,7 +105,8 @@ module JekyllImgFlow
104
105
  # Process the operation
105
106
  operation = {
106
107
  type: task[:operation_type],
107
- params: task[:params]
108
+ params: task[:params],
109
+ file_digest: task[:file_digest]
108
110
  }
109
111
 
110
112
  Jekyll.logger.debug "🔍 BatchManager: Processing task - input: #{task[:input_path]}, original: #{task[:original_name]}"
@@ -201,7 +203,7 @@ module JekyllImgFlow
201
203
  # @param config [Config] ImgFlow configuration
202
204
  # @param site [Jekyll::Site] Jekyll site object
203
205
  # @return [Array<Hash>] Array of tasks
204
- def self.build_default_tasks(original_name, input_path, config, _site)
206
+ def self.build_default_tasks(original_name, input_path, config, _site, file_digest: nil)
205
207
  tasks = []
206
208
 
207
209
  # Use FilenameGenerator for consistent naming
@@ -233,6 +235,7 @@ module JekyllImgFlow
233
235
  },
234
236
  version_type: :default,
235
237
  page_path: nil, # Default versions not tied to specific page
238
+ file_digest: file_digest,
236
239
  skip_if_exists: true
237
240
  }
238
241
  end
@@ -13,29 +13,31 @@ module JekyllImgFlow
13
13
 
14
14
  # Initialize operation processor with current provider
15
15
  provider = @registry.current_provider
16
- @operation_processor = OperationProcessor.new(provider, @path_resolver, @manifest, @config)
16
+ @operation_processor = OperationProcessor.new(provider, @path_resolver, @manifest, @config) if provider
17
17
 
18
18
  # Initialize batch manager
19
19
  @batch_manager = BatchManager.new(@operation_processor)
20
20
 
21
21
  # Store all components in site for sharing with ImgFlow tag
22
- return if @site.imgflow_components
23
-
24
- filename_generator = JekyllImgFlow::FilenameGenerator.new
22
+ @filename_generator = JekyllImgFlow::FilenameGenerator.new
25
23
 
26
24
  @site.imgflow_components = {
27
25
  config: @config,
28
26
  manifest: @manifest,
29
27
  path_resolver: @path_resolver,
30
- filename_generator: filename_generator,
28
+ filename_generator: @filename_generator,
31
29
  registry: @registry,
32
30
  provider: provider,
33
- operation_processor: @operation_processor
31
+ operation_processor: @operation_processor,
32
+ stats: @operation_processor&.stats,
33
+ preset_manager: JekyllImgFlow::PresetManager.new(@site, @config)
34
34
  }
35
35
  end
36
36
 
37
37
  # Process all changed images in originals folder
38
38
  def process_changed_images
39
+ return { completed: 0, failed: 0 } unless @operation_processor
40
+
39
41
  Jekyll.logger.info "🔄 BuildTimeProcessor: Processing changed images"
40
42
 
41
43
  # Get all original images
@@ -44,27 +46,37 @@ module JekyllImgFlow
44
46
 
45
47
  originals_dir = File.join(@site.source, @config.originals)
46
48
 
49
+ # Clean up manifest entries for deleted originals
50
+ current_originals = original_paths.map { |path| path.sub("#{originals_dir}/", "") }
51
+ @manifest.cleanup_deleted_originals(current_originals)
52
+ @manifest.cleanup_obsolete_defaults(expected_default_operations)
53
+ @manifest.reset_page_usage unless incremental_build?
54
+
47
55
  # Build tasks for each original image
48
- original_paths.each do |path|
56
+ original_paths.reject { |path| AnimatedGifDetector.animated?(path) }.each do |path|
49
57
  original_name = path.sub("#{originals_dir}/", "")
58
+ file_digest = @filename_generator.file_digest(path)
50
59
 
51
60
  # Check if needs processing (file changed or provider changed)
52
- if needs_processing?(original_name, path)
61
+ if needs_processing?(original_name, path, file_digest)
53
62
  Jekyll.logger.info "🔧 Queuing default versions for: #{original_name}"
54
63
 
55
64
  # Build default tasks
56
- tasks = BatchManager.build_default_tasks(original_name, path, @config, @site)
65
+ tasks = BatchManager.build_default_tasks(
66
+ original_name, path, @config, @site, file_digest: file_digest
67
+ )
68
+ force_processing = source_digest_changed?(original_name, file_digest)
69
+ tasks.each { |task| task[:force_processing] = force_processing }
57
70
  @batch_manager.add_tasks(tasks)
58
71
  else
72
+ expected_default_operations.length.times { @operation_processor.stats.record_cache_hit }
59
73
  Jekyll.logger.debug "⏭️ Skipping #{original_name} - already up-to-date"
60
74
  end
61
75
  end
62
76
 
63
77
  # Process all queued tasks
64
78
  results = @batch_manager.process_all
65
-
66
- # Register completed tasks in manifest
67
- register_completed_tasks(@batch_manager.completed)
79
+ register_skipped_tasks(@batch_manager.completed)
68
80
 
69
81
  # Save manifest so it's available when ImgflowTag runs
70
82
  @manifest.save
@@ -76,34 +88,19 @@ module JekyllImgFlow
76
88
 
77
89
  private
78
90
 
79
- # Register completed tasks in manifest
80
- # @param completed_tasks [Array<Hash>] Array of completed tasks from BatchManager
81
- def register_completed_tasks(completed_tasks)
82
- completed_tasks.each do |task|
83
- next unless task[:task] && task[:result]
84
-
85
- # Get provider name
86
- provider_name = @registry.current_provider.class.name.split("::").last.downcase
91
+ def register_skipped_tasks(completed_tasks)
92
+ provider_name = @registry.current_provider&.class&.provider_name || "unknown"
93
+ completed_tasks.each do |completed|
94
+ next unless completed[:status] == :skipped
87
95
 
88
- # Convert absolute path to relative for manifest storage
89
- # Default versions are written to the source directory so Jekyll can
90
- # copy them to _site during the write phase.
91
- absolute_path = task[:result]
92
- relative_path = absolute_path.sub(@site.source, "")
93
-
94
- # Register the version in manifest
96
+ task = completed[:task]
97
+ @operation_processor.stats.record_cache_hit
98
+ relative_path = task[:output_path].sub(@site.source, "")
95
99
  @manifest.register_version(
96
- task[:task][:original_name],
97
- relative_path,
98
- task[:task][:params],
99
- :default, # BuildTimeProcessor creates default versions
100
- nil, # page_path (default versions aren't tied to specific pages)
101
- nil, # file_digest
102
- provider_name
100
+ task[:original_name], relative_path, task[:params], :default, nil,
101
+ task[:file_digest], provider_name
103
102
  )
104
103
  end
105
-
106
- Jekyll.logger.debug "📝 Registered #{completed_tasks.length} completed tasks in manifest"
107
104
  end
108
105
 
109
106
  # Find all original images in originals folder
@@ -113,48 +110,60 @@ module JekyllImgFlow
113
110
  input_formats = @config.input_formats
114
111
 
115
112
  Dir.glob(File.join(originals_dir, "**", "*.{#{input_formats.join(',')}}"))
116
- .reject { |path| AnimatedGifDetector.animated?(path) }
117
113
  end
118
114
 
119
115
  # Check if image needs processing
120
116
  # @param original_name [String] Path to original image relative to originals directory
121
117
  # @param path [String, nil] Full path to original image (defaults to original_name)
122
118
  # @return [Boolean] True if processing needed
123
- def needs_processing?(original_name, path = nil)
119
+ def needs_processing?(original_name, path = nil, file_digest = nil)
124
120
  path ||= original_name
125
- # For single-argument calls, derive a flat original_name from the full path
126
121
  original_name = File.basename(original_name) if original_name == path
122
+ return true unless File.file?(path)
127
123
 
128
- # If no versions exist, needs processing
129
- return true unless @manifest.versions?(original_name)
124
+ file_digest ||= @filename_generator.file_digest(path)
125
+ default_versions = @manifest.get_versions(original_name)["default"] || []
126
+ current_provider = @registry.current_provider&.class&.provider_name || "unknown"
130
127
 
131
- # Check if original file was modified
132
- versions = @manifest.get_versions(original_name)
133
- default_versions = versions["default"] || []
134
-
135
- return true if default_versions.empty?
128
+ expected_default_operations.any? do |operations|
129
+ version = default_versions.find do |candidate|
130
+ @manifest.same_operations?(candidate["operations"], operations)
131
+ end
132
+ !current_default_version?(version, current_provider, file_digest)
133
+ end
134
+ end
136
135
 
137
- # Get the most recent version creation time
138
- latest_version = default_versions.max_by { |v| v["created_at"] || 0 }
139
- version_time = latest_version["created_at"]
136
+ def expected_default_operations
137
+ @config.sizes.each_value.flat_map do |width|
138
+ @config.formats.map do |format|
139
+ { width: width, format: format, quality: @config.quality }
140
+ end
141
+ end
142
+ end
140
143
 
141
- # If original is newer than versions, needs processing
142
- return true unless version_time
144
+ def source_digest_changed?(original_name, file_digest)
145
+ digests = (@manifest.get_versions(original_name)["default"] || []).filter_map do |version|
146
+ version["file_digest"]
147
+ end
148
+ digests.any? && digests.any? { |digest| digest != file_digest }
149
+ end
143
150
 
144
- original_mtime = File.mtime(path).to_i
145
- return true if original_mtime > version_time
151
+ def incremental_build?
152
+ @site.is_a?(Jekyll::Site) && @site.incremental?
153
+ end
146
154
 
147
- # No timestamp means we need to process
155
+ def current_default_version?(version, current_provider, file_digest)
156
+ return false unless version && version["provider"] == current_provider
157
+ return false unless version["file_digest"] == file_digest
148
158
 
149
- # Check if provider changed
150
- current_provider = @registry.current_provider.class.provider_name
151
- version_providers = default_versions.map { |v| v["provider"] }
152
- version_providers.uniq!
159
+ output_path = version["output"]
160
+ return false unless output_path
153
161
 
154
- return true unless version_providers.include?(current_provider)
162
+ absolute_path = File.expand_path(output_path.delete_prefix("/"), @site.source)
163
+ output_root = File.expand_path(@config.output, @site.source)
164
+ return false unless absolute_path.start_with?("#{output_root}#{File::SEPARATOR}")
155
165
 
156
- # Up-to-date
157
- false
166
+ File.file?(absolute_path)
158
167
  end
159
168
  end
160
169
  end
@@ -23,7 +23,7 @@ module JekyllImgFlow
23
23
 
24
24
  attr_reader :site, :originals, :output, :input_formats, :sizes, :formats,
25
25
  :quality, :backend_priority, :imgproxy_url,
26
- :image_compressor_url, :weserv_url, :flyimg_url,
26
+ :weserv_url, :flyimg_url,
27
27
  :optimize_qualities, :fallback_format
28
28
 
29
29
  def initialize(site)
@@ -42,7 +42,6 @@ module JekyllImgFlow
42
42
  @fallback_format = cfg["fallback_format"] || DEFAULT_FALLBACK_FORMAT
43
43
 
44
44
  @imgproxy_url = cfg["imgproxy_url"]
45
- @image_compressor_url = cfg["image_compressor_url"]
46
45
  @weserv_url = cfg["weserv_url"]
47
46
  @flyimg_url = cfg["flyimg_url"]
48
47
  @optimize_qualities = cfg["optimize_qualities"] || {
@@ -64,12 +63,10 @@ module JekyllImgFlow
64
63
  {
65
64
  enabled: merged_config.key?("imgproxy_url") ||
66
65
  merged_config.key?("weserv_url") ||
67
- merged_config.key?("flyimg_url") ||
68
- merged_config.key?("image_compressor_url"),
66
+ merged_config.key?("flyimg_url"),
69
67
  imgproxy_url: @imgproxy_url,
70
68
  weserv_url: @weserv_url,
71
- flyimg_url: @flyimg_url,
72
- image_compressor_url: @image_compressor_url
69
+ flyimg_url: @flyimg_url
73
70
  }
74
71
  end
75
72
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "digest"
4
- require "json"
5
4
 
6
5
  module JekyllImgFlow
7
6
  # Generates unique filenames for processed images
@@ -11,10 +10,18 @@ module JekyllImgFlow
11
10
  # @param original_name [String] Original image filename
12
11
  # @param operations [Hash] Operations applied to image
13
12
  # @return [String] Generated filename (without path)
13
+ # Formats that are valid for output (raster only).
14
+ # SVG and other vector formats are input-only — image providers
15
+ # always produce raster output (webp, avif, jpg, png).
16
+ VECTOR_FORMATS = %w[svg].freeze
17
+
14
18
  def generate_filename(original_name, operations)
15
19
  base_name = File.basename(original_name, ".*")
16
20
  width = operations[:width]
17
21
  format = operations[:format] || File.extname(original_name).delete(".")
22
+ # Vector formats (e.g. SVG) are input-only — never use them as
23
+ # output format. Fall back to jpg (universal <img> fallback).
24
+ format = "jpg" if VECTOR_FORMATS.include?(format.to_s.downcase)
18
25
 
19
26
  # Generate hash using Jekyll Picture Tag approach
20
27
  hash = generate_jpt_hash(original_name, operations)
@@ -22,15 +29,6 @@ module JekyllImgFlow
22
29
  "#{base_name}-#{width}-#{hash}.#{format}"
23
30
  end
24
31
 
25
- # Generate cache key for operations (SHA256 for manifest tracking)
26
- # @param operations [Hash] Operations hash
27
- # @return [String] Cache key
28
- def generate_cache_key(operations)
29
- # Sort keys to ensure consistent hashing
30
- sorted_operations = operations.sort.to_h
31
- Digest::SHA256.hexdigest(sorted_operations.to_json)
32
- end
33
-
34
32
  # Get file digest for manifest tracking
35
33
  # @param original_path [String] Path to original image
36
34
  # @return [String] SHA256 digest of file
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module JekyllImgFlow
6
+ class GeneratedFileCleaner
7
+ def initialize(site, config)
8
+ @site = site
9
+ @config = config
10
+ end
11
+
12
+ def delete(output_path)
13
+ return false unless output_path
14
+
15
+ unregister_static_file(output_path)
16
+ deleted = false
17
+ [@site.source, @site.dest].each do |base_path|
18
+ path = safe_generated_path(base_path, output_path)
19
+ next unless path && File.file?(path)
20
+
21
+ File.delete(path)
22
+ remove_empty_output_dirs(File.dirname(path), output_root(base_path))
23
+ deleted = true
24
+ end
25
+ deleted
26
+ end
27
+
28
+ def clear
29
+ roots = [@site.source, @site.dest].map { |base_path| output_root(base_path) }
30
+ valid = roots.zip([@site.source, @site.dest]).all? do |directory, base_path|
31
+ base = File.expand_path(base_path)
32
+ directory.start_with?("#{base}#{File::SEPARATOR}")
33
+ end
34
+ raise "Refusing to delete optimized directory outside configured roots" unless valid
35
+
36
+ unregister_output_tree(roots.first)
37
+ files = roots.flat_map do |directory|
38
+ Dir.glob(File.join(directory, "**", "*")).select { |path| File.file?(path) }
39
+ end
40
+ files = files.uniq
41
+ files.each { |path| File.delete(path) }
42
+ files.length
43
+ end
44
+
45
+ private
46
+
47
+ def unregister_static_file(output_path)
48
+ return unless @site.is_a?(Jekyll::Site)
49
+
50
+ source_path = safe_generated_path(@site.source, output_path)
51
+ @site.static_files.reject! do |static_file|
52
+ source_path && File.expand_path(static_file.path) == source_path
53
+ end
54
+ end
55
+
56
+ def unregister_output_tree(root)
57
+ return unless @site.is_a?(Jekyll::Site)
58
+
59
+ @site.static_files.reject! do |static_file|
60
+ File.expand_path(static_file.path).start_with?("#{root}#{File::SEPARATOR}")
61
+ end
62
+ end
63
+
64
+ def safe_generated_path(base_path, output_path)
65
+ root = output_root(base_path)
66
+ candidate = File.expand_path(output_path.delete_prefix("/"), base_path)
67
+ candidate if candidate.start_with?("#{root}#{File::SEPARATOR}")
68
+ end
69
+
70
+ def output_root(base_path)
71
+ File.expand_path(@config.output, base_path)
72
+ end
73
+
74
+ def remove_empty_output_dirs(directory, root)
75
+ while directory.start_with?("#{root}#{File::SEPARATOR}") && Dir.empty?(directory)
76
+ Dir.rmdir(directory)
77
+ directory = File.dirname(directory)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -11,6 +11,12 @@ module JekyllImgFlow
11
11
  site.define_singleton_method(:imgflow_components=) { |value| @imgflow_components = value }
12
12
  end
13
13
 
14
+ if site.respond_to?(:config)
15
+ cache_dir = Config.new(site).cache_dir
16
+ site.config["exclude"] = Array(site.config["exclude"])
17
+ site.config["exclude"] << cache_dir unless site.config["exclude"].include?(cache_dir)
18
+ end
19
+
14
20
  # Components will be initialized on first use (lazy loading)
15
21
  Jekyll.logger.info "✅ ImgFlow: Components ready for initialization"
16
22
  end
@@ -31,22 +37,23 @@ module JekyllImgFlow
31
37
  Jekyll.logger.info "🔌 ImgFlow post_write hook - saving final manifest with page usage"
32
38
 
33
39
  if site.imgflow_components && site.imgflow_components[:manifest]
34
- site.imgflow_components[:manifest].save
40
+ manifest = site.imgflow_components[:manifest]
41
+ cleanup_orphaned_images(site, manifest) if Jekyll.env != "development" &&
42
+ !(site.is_a?(Jekyll::Site) && site.incremental?)
43
+ manifest.save
35
44
  Jekyll.logger.info "✅ ImgFlow: Final manifest saved with all page usage data"
36
45
  else
37
46
  Jekyll.logger.warn "⚠️ ImgFlow: No manifest found to save"
38
47
  end
39
-
40
- # Clean up orphaned specialized images in production
41
- cleanup_orphaned_images(site) if Jekyll.env != "development"
42
48
  end
43
49
 
44
- def self.cleanup_orphaned_images(site)
50
+ def self.cleanup_orphaned_images(site, manifest = nil)
45
51
  Jekyll.logger.info "🧹 ImgFlow: Checking for orphaned specialized images..."
46
52
 
47
53
  # Use ManifestManager to find and cleanup orphans
48
- manifest = ManifestManager.new(site)
54
+ manifest ||= ManifestManager.new(site)
49
55
  cleaned = manifest.cleanup_orphans
56
+ manifest.save
50
57
 
51
58
  if cleaned.any?
52
59
  Jekyll.logger.info "✅ ImgFlow: Removed #{cleaned.length} orphaned specialized images"
@@ -4,9 +4,6 @@ module JekyllImgFlow
4
4
  # Unified HTML Generator for all markup formats
5
5
  # Supports Picture Tag compatibility with multiple markup formats and attributes
6
6
  class HtmlGenerator
7
- MARKUP_FORMATS = %w[auto picture img data_auto data_picture data_img direct_url
8
- naked_srcset].freeze
9
-
10
7
  # Generate HTML based on markup format and attributes
11
8
  # @param results [Array<String>] Processed image paths
12
9
  # @param attributes [Hash] HTML attributes by element
@@ -332,11 +329,20 @@ module JekyllImgFlow
332
329
  .gsub(">", "&gt;")
333
330
  end
334
331
 
335
- # Get correct path for HTML generation (relative paths for Jekyll sites)
332
+ # Get correct path for HTML generation.
336
333
  def html_path(relative_path)
337
- # For HTML generation, we want relative paths within the site
338
- # Remove leading slash to make it relative to site root
339
- relative_path.start_with?("/") ? relative_path[1..] : relative_path
334
+ path = "/#{relative_path.delete_prefix('/')}"
335
+ baseurl = html_baseurl
336
+ return path if baseurl.empty? || path == baseurl || path.start_with?("#{baseurl}/")
337
+
338
+ "#{baseurl}#{path}"
339
+ end
340
+
341
+ def html_baseurl
342
+ site = @context&.registers&.dig(:site) || @config&.site
343
+ baseurl = site&.config&.fetch("baseurl", "").to_s
344
+ normalized = baseurl.gsub(%r{\A/+|/+$}, "")
345
+ normalized.empty? ? "" : "/#{normalized}"
340
346
  end
341
347
 
342
348
  # Get absolute URL when needed (for direct_url format)
@@ -345,18 +351,7 @@ module JekyllImgFlow
345
351
 
346
352
  site = @context.registers[:site]
347
353
  base_url = site.config["url"] || "http://localhost:4000"
348
- baseurl = site.config["baseurl"] || ""
349
-
350
- # Ensure base_url ends without trailing slash and baseurl starts without slash
351
- base_url = base_url.chomp("/")
352
- baseurl = baseurl[1..] if baseurl.start_with?("/")
353
-
354
- # Build URL parts carefully to avoid double slashes
355
- url_parts = [base_url]
356
- url_parts << baseurl unless baseurl.empty?
357
- url_parts << html_path(relative_path)
358
-
359
- url_parts.join("/")
354
+ "#{base_url.chomp('/')}#{html_path(relative_path)}"
360
355
  end
361
356
  end
362
357
  end