jekyll-uj-powertools 1.6.23 → 1.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f8e4f51fc9ea4a48db55c09c67dc157e8ff10a2ac3ec70b0725cd691f22e8eeb
4
- data.tar.gz: bd0025c8a7b871ce18e261c7b7a11d9eec2dfa756769905682eaecc681771d27
3
+ metadata.gz: 60803db8fb2a1e2c50aea11a0f49e964fc16d3323943ce0330f4312fd6468e37
4
+ data.tar.gz: 05f5ac27b7651b0d5592395cc510ba5b31e77f9b5ead9f35f0de6420d5919fd9
5
5
  SHA512:
6
- metadata.gz: 265cb3b441bf454a72cb0dd80fd7357c8a775e91602969b8ed2f35068825baf3b48571fbbe7841ca801a694f17c2c7f2bb107a7c860af1c965e369448c4f6d2a
7
- data.tar.gz: 60a90ebbfc3a02462063ade9848b1f58ed50596a525a6a1fd9d4547de89cb048dbd712e8b9f6dedfee86e1003365a021fc2a615a4a9938914c94d6901dc9e210
6
+ metadata.gz: 17bb830ad04f8be16741f99f9dcaab5f723b7c9bb58f1ffbdfa0bf9a9637c0357e829c82dd4dc13feee2e70773bde48cd71e1cface15873f7232a60ea6880f4c
7
+ data.tar.gz: 21c374abbaa0e43c40bf1ecda75ac52eda70dd33140c69c972bf352eca2d571c35c5d70485947275a639c46b7df8e235f496a2b678a29008722ebeadb9e4e62b
data/README.md CHANGED
@@ -294,6 +294,90 @@ Creates language-specific URLs for multilingual sites.
294
294
  {% uj_translation_url target_lang, "/pricing" %}
295
295
  ```
296
296
 
297
+ ## Generators
298
+
299
+ ### Dynamic Pages Generator
300
+ Automatically generate pages from collection data based on frontmatter fields. This is useful for creating category, tag, or any taxonomy pages dynamically without manually creating each page.
301
+
302
+ #### Configuration
303
+ Add to your `_config.yml`:
304
+
305
+ ```yaml
306
+ generators:
307
+ collection_categories:
308
+ # Example 1: Extract category from nested frontmatter field
309
+ - collection: recipes
310
+ field: recipe.cuisine # Dot notation for nested fields
311
+ layout: recipe-category
312
+ permalink: /recipes/:slug
313
+ title: ":name Recipes"
314
+ description: "Browse our collection of :name recipes."
315
+
316
+ # Example 2: Simple frontmatter field
317
+ - collection: products
318
+ field: category
319
+ layout: product-category
320
+ permalink: /products/:slug
321
+ title: ":name Products"
322
+ description: "Shop our :name products."
323
+
324
+ # Example 3: Minimal config (uses defaults)
325
+ - collection: articles
326
+ field: category
327
+ layout: article-category
328
+ ```
329
+
330
+ #### Template Variables
331
+ Use these placeholders in `title`, `description`, and `permalink`:
332
+ - `:name` - The formatted category name (e.g., "Asian Cuisine")
333
+ - `:slug` - The URL-safe slug (e.g., "asian-cuisine")
334
+
335
+ #### Defaults
336
+ - `permalink`: `/:collection/:slug` (e.g., `/recipes/asian`)
337
+ - `title`: `:name`
338
+ - `description`: `Browse our collection of :name.`
339
+
340
+ #### Generated Page Data
341
+ Each generated page includes the following data accessible in the layout:
342
+ - `page.title` - The formatted title
343
+ - `page.description` - The formatted description
344
+ - `page.category_slug` - URL-safe slug (e.g., "asian-cuisine")
345
+ - `page.category_name` - Human-readable name (e.g., "Asian Cuisine")
346
+ - `page.collection_name` - Source collection name (e.g., "recipes")
347
+ - `page.meta.title` - SEO title with site name appended
348
+ - `page.meta.description` - SEO description
349
+
350
+ #### Example Layout
351
+ Create a layout file (e.g., `_layouts/recipe-category.html`) to display the category page:
352
+
353
+ ```liquid
354
+ ---
355
+ layout: default
356
+ ---
357
+ <h1>{{ page.category_name }}</h1>
358
+ <p>{{ page.description }}</p>
359
+
360
+ {% assign items = site.recipes | where_exp: "item", "item.recipe.cuisine == page.category_name" %}
361
+ {% for item in items %}
362
+ <article>
363
+ <h2><a href="{{ item.url }}">{{ item.title }}</a></h2>
364
+ </article>
365
+ {% endfor %}
366
+ ```
367
+
368
+ ## Development Config (`_config.dev.yml`)
369
+ Speed up dev builds by limiting collections. Create `_config.dev.yml` in your Jekyll source:
370
+
371
+ ```yaml
372
+ limit_collections:
373
+ recipes: 50
374
+ products: 20
375
+ ```
376
+
377
+ Run with: `bundle exec jekyll serve --config _config.yml,_config.dev.yml`
378
+
379
+ UJ auto-loads this file in dev mode.
380
+
297
381
  ## Final notes
298
382
  These examples show how you can use the features of `jekyll-uj-powertools` in your Jekyll site.
299
383
 
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  Gem::Specification.new do |spec|
6
6
  # Gem info
7
7
  spec.name = "jekyll-uj-powertools"
8
- spec.version = "1.6.23"
8
+ spec.version = "1.7.0"
9
9
 
10
10
  # Author info
11
11
  spec.authors = ["ITW Creative Works"]
@@ -0,0 +1,152 @@
1
+ # Dynamic Pages Generator
2
+ # Automatically generates pages from collection data based on config
3
+ #
4
+ # Usage in _config.yml:
5
+ #
6
+ # generators:
7
+ # collection_categories:
8
+ # # Example 1: Extract category from frontmatter field
9
+ # - collection: recipes
10
+ # field: recipe.cuisine # Dot notation for nested fields
11
+ # layout: recipe-category
12
+ # permalink: /recipes/:slug
13
+ # title: ":name Recipes"
14
+ # description: "Browse our collection of :name recipes."
15
+ #
16
+ # # Example 2: Simple frontmatter field
17
+ # - collection: products
18
+ # field: category
19
+ # layout: product-category
20
+ # permalink: /products/:slug
21
+ # title: ":name Products"
22
+ # description: "Shop our :name products."
23
+ #
24
+ # # Example 3: Minimal config (uses defaults)
25
+ # - collection: articles
26
+ # field: category
27
+ # layout: article-category
28
+ #
29
+ # Template variables for title/description/permalink:
30
+ # :name - The formatted category name (e.g., "Asian Cuisine")
31
+ # :slug - The URL-safe slug (e.g., "asian-cuisine")
32
+ #
33
+ # Defaults:
34
+ # permalink: /:collection/:slug (e.g., /recipes/asian)
35
+ # title: :name
36
+ # description: "Browse our collection of :name."
37
+
38
+ module Jekyll
39
+ class DynamicPagesGenerator < Generator
40
+ safe true
41
+ priority :normal # Run before InjectProperties (:low) so dynamic pages get resolved data
42
+
43
+ def generate(site)
44
+ config = site.config.dig('generators', 'collection_categories')
45
+ return unless config.is_a?(Array)
46
+
47
+ config.each do |category_config|
48
+ generate_category_pages(site, category_config)
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def generate_category_pages(site, config)
55
+ collection_name = config['collection']
56
+ field = config['field']
57
+ return unless collection_name && field
58
+
59
+ collection = site.collections[collection_name]
60
+ return unless collection
61
+
62
+ # Configuration with defaults
63
+ layout = config['layout']
64
+ permalink_template = config['permalink'] || "/#{collection_name}/:slug"
65
+ title_template = config['title'] || ':name'
66
+ description_template = config['description'] || 'Browse our collection of :name.'
67
+
68
+ # Extract unique categories from documents
69
+ categories = {}
70
+
71
+ collection.docs.each do |doc|
72
+ category_value = dig_value(doc.data, field)
73
+ next unless category_value && !category_value.empty?
74
+
75
+ category_slug = slugify(category_value)
76
+ next if categories.key?(category_slug)
77
+
78
+ # Format category name: titleize
79
+ category_name = titleize(category_value)
80
+
81
+ # Apply templates
82
+ permalink = apply_template(permalink_template, category_name, category_slug)
83
+ title = apply_template(title_template, category_name, category_slug)
84
+ description = apply_template(description_template, category_name, category_slug)
85
+
86
+ categories[category_slug] = {
87
+ 'slug' => category_slug,
88
+ 'name' => category_name,
89
+ 'title' => title,
90
+ 'description' => description,
91
+ 'permalink' => permalink
92
+ }
93
+ end
94
+
95
+ # Generate a page for each category
96
+ categories.each do |slug, data|
97
+ page = DynamicCategoryPage.new(site, layout, data, collection_name)
98
+ site.pages << page
99
+ end
100
+
101
+ Jekyll.logger.info "DynamicPages:", "Generated #{categories.size} category pages for '#{collection_name}'"
102
+ end
103
+
104
+ # Dig into nested hash using dot notation (e.g., "recipe.cuisine")
105
+ def dig_value(hash, field)
106
+ keys = field.split('.')
107
+ value = hash
108
+ keys.each do |key|
109
+ return nil unless value.is_a?(Hash)
110
+ value = value[key]
111
+ end
112
+ value.is_a?(String) ? value : nil
113
+ end
114
+
115
+ def slugify(str)
116
+ str.to_s.downcase.strip.gsub(/[^\w\s-]/, '').gsub(/[\s_]+/, '-')
117
+ end
118
+
119
+ def titleize(str)
120
+ str.to_s.split(/[\s_-]+/).map(&:capitalize).join(' ')
121
+ end
122
+
123
+ def apply_template(template, name, slug)
124
+ template.gsub(':name', name).gsub(':slug', slug)
125
+ end
126
+ end
127
+
128
+ # Custom page class for dynamically generated category pages
129
+ class DynamicCategoryPage < Page
130
+ def initialize(site, layout, data, collection_name)
131
+ @site = site
132
+ @base = site.source
133
+ @dir = ''
134
+ @name = "#{data['slug']}.html"
135
+
136
+ self.process(@name)
137
+ self.data = {
138
+ 'layout' => layout,
139
+ 'title' => data['title'],
140
+ 'description' => data['description'],
141
+ 'category_slug' => data['slug'],
142
+ 'category_name' => data['name'],
143
+ 'collection_name' => collection_name,
144
+ 'permalink' => data['permalink'],
145
+ 'meta' => {
146
+ 'title' => "#{data['title']} - #{site.config.dig('brand', 'name') || site.config['title']}",
147
+ 'description' => data['description']
148
+ }
149
+ }
150
+ end
151
+ end
152
+ end
@@ -5,7 +5,7 @@
5
5
  module Jekyll
6
6
  class InjectProperties < Generator
7
7
  safe true
8
- priority :low
8
+ priority :lowest # Run LAST so all pages from other generators exist before injecting resolved data
9
9
 
10
10
  def generate(site)
11
11
  # Define a global variable accessible in templates
@@ -0,0 +1,56 @@
1
+ # Limit Collections Generator
2
+ # Limits the number of documents in collections during development for faster builds
3
+ #
4
+ # Usage in _config.dev.yml:
5
+ # limit_collections:
6
+ # recipes: 50
7
+ # products: 20
8
+ #
9
+ # By default, uses a seeded random sample for diverse selection across categories.
10
+ # To disable randomization and take first N documents:
11
+ # limit_collections:
12
+ # recipes: 50
13
+ # products: 20
14
+ # randomize: false
15
+ #
16
+ # Run Jekyll with: bundle exec jekyll serve --config _config.yml,_config.dev.yml
17
+
18
+ module Jekyll
19
+ class LimitCollectionsGenerator < Generator
20
+ safe true
21
+ priority :highest # Run before other generators
22
+
23
+ def generate(site)
24
+ limits = site.config['limit_collections']
25
+ return unless limits.is_a?(Hash)
26
+
27
+ # Check if randomization is disabled (default: true)
28
+ randomize = limits.fetch('randomize', true)
29
+
30
+ limits.each do |collection_name, limit|
31
+ # Skip the 'randomize' option itself
32
+ next if collection_name == 'randomize'
33
+ next unless limit.is_a?(Integer) && limit > 0
34
+
35
+ collection = site.collections[collection_name]
36
+ next unless collection
37
+
38
+ original_count = collection.docs.size
39
+ next if original_count <= limit
40
+
41
+ if randomize
42
+ # Use a seeded random for repeatable but diverse sampling
43
+ # Seed based on collection name so it's consistent across rebuilds
44
+ rng = Random.new(collection_name.hash.abs)
45
+ sampled_docs = collection.docs.shuffle(random: rng).first(limit)
46
+ collection.docs.replace(sampled_docs)
47
+ Jekyll.logger.info "LimitCollections:", "Limited '#{collection_name}' from #{original_count} to #{limit} documents (random sample)"
48
+ else
49
+ # Take first N documents in order
50
+ collection.docs.replace(collection.docs.first(limit))
51
+ Jekyll.logger.info "LimitCollections:", "Limited '#{collection_name}' from #{original_count} to #{limit} documents"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -6,8 +6,10 @@ module Jekyll
6
6
  require_relative "filters/main"
7
7
 
8
8
  # Load Generators
9
+ require_relative "generators/limit-collections"
9
10
  require_relative "generators/inject-properties"
10
11
  require_relative "generators/blog-taxonomy"
12
+ require_relative "generators/dynamic-pages"
11
13
 
12
14
  # Load Hooks
13
15
  require_relative "hooks/inject-properties"
data/lib/tags/logo.rb CHANGED
@@ -5,12 +5,15 @@ require_relative '../helpers/variable_resolver'
5
5
  module Jekyll
6
6
  class UJLogoTag < Liquid::Tag
7
7
  include UJPowertools::VariableResolver
8
-
8
+
9
9
  # Default logo to show when requested logo is not found
10
10
  DEFAULT_LOGO = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M320 64C334.7 64 348.2 72.1 355.2 85L571.2 485C577.9 497.4 577.6 512.4 570.4 524.5C563.2 536.6 550.1 544 536 544L104 544C89.9 544 76.9 536.6 69.6 524.5C62.3 512.4 62.1 497.4 68.8 485L284.8 85C291.8 72.1 305.3 64 320 64zM320 232C306.7 232 296 242.7 296 256L296 368C296 381.3 306.7 392 320 392C333.3 392 344 381.3 344 368L344 256C344 242.7 333.3 232 320 232zM346.7 448C347.3 438.1 342.4 428.7 333.9 423.5C325.4 418.4 314.7 418.4 306.2 423.5C297.7 428.7 292.8 438.1 293.4 448C292.8 457.9 297.7 467.3 306.2 472.5C314.7 477.6 325.4 477.6 333.9 472.5C342.4 467.3 347.3 457.9 346.7 448z"/></svg>'
11
-
12
- # Cache for loaded logos to improve performance
11
+
12
+ # Cache for loaded logos (raw SVG content) to improve performance
13
13
  @@logo_cache = {}
14
+
15
+ # Counter for generating unique prefixes per logo instance
16
+ @@instance_counter = 0
14
17
 
15
18
  def initialize(tag_name, markup, tokens)
16
19
  super
@@ -63,22 +66,27 @@ module Jekyll
63
66
  # Get site from context
64
67
  site = context.registers[:site]
65
68
  return '' unless site
66
-
67
- # Load the logo SVG from file
68
- logo_svg = load_logo_from_file(logo_name.to_s, type, color)
69
- return '' unless logo_svg
70
-
71
- # Return the SVG directly
72
- logo_svg
69
+
70
+ # Load the raw logo SVG from file (cached)
71
+ raw_svg = load_logo_from_file(logo_name.to_s, type, color)
72
+ return '' unless raw_svg
73
+
74
+ # Generate unique prefix for this instance to prevent ID conflicts
75
+ # when the same logo is used multiple times on a page
76
+ @@instance_counter += 1
77
+ unique_prefix = "#{logo_name}-#{@@instance_counter}"
78
+
79
+ # Prefix all IDs with unique prefix
80
+ prefix_svg_ids(raw_svg, unique_prefix)
73
81
  end
74
-
82
+
75
83
  private
76
-
84
+
77
85
  def load_logo_from_file(logo_name, type, color)
78
86
  # Create cache key
79
87
  cache_key = "#{type}/#{color}/#{logo_name}"
80
88
 
81
- # Return cached version if available
89
+ # Return cached version if available (raw SVG without prefixing)
82
90
  return @@logo_cache[cache_key] if @@logo_cache.key?(cache_key)
83
91
 
84
92
  # Build file path
@@ -91,10 +99,7 @@ module Jekyll
91
99
  DEFAULT_LOGO
92
100
  end
93
101
 
94
- # Prefix all IDs to prevent conflicts when multiple SVGs are on the same page
95
- logo_svg = prefix_svg_ids(logo_svg, logo_name)
96
-
97
- # Cache the result
102
+ # Cache the raw result (before prefixing)
98
103
  @@logo_cache[cache_key] = logo_svg
99
104
  return logo_svg
100
105
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-uj-powertools
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.23
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ITW Creative Works
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-12-17 00:00:00.000000000 Z
11
+ date: 2025-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -115,7 +115,9 @@ files:
115
115
  - jekyll-uj-powertools.gemspec
116
116
  - lib/filters/main.rb
117
117
  - lib/generators/blog-taxonomy.rb
118
+ - lib/generators/dynamic-pages.rb
118
119
  - lib/generators/inject-properties.rb
120
+ - lib/generators/limit-collections.rb
119
121
  - lib/helpers/variable_resolver.rb
120
122
  - lib/hooks/inject-properties.rb
121
123
  - lib/hooks/markdown-images.rb