jekyll-notion-cms 1.0.1 → 1.0.3

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: 68a78b984aaaa3dddb7132d791884c84b01698e2c0f2b49c3333e2637e83e67f
4
- data.tar.gz: 2d83bc41a51f82b90c8dd9fc6a76be86eb35ea627d0aa74d9be67381868bb792
3
+ metadata.gz: 33cff8a4d2a08b099224d2db5605438b6a49e3a87c83b1d18f3679d19def3a31
4
+ data.tar.gz: 6efe8bd404d78a1816e0f4ed5f0cfa4dbc0f2820d3bc363996ae8e3d82bdd7f9
5
5
  SHA512:
6
- metadata.gz: c390711580fc795641e3392babe4c73e078c0b909492797f7650fa7bf66608b2d6c88c51cb07c23e71f843e3c39305f5d69119516e66289dfde9201a90ff54ae
7
- data.tar.gz: 9afbc03649b5f2d6359d7d0c4670edc2eb06ae630542e0e5d9bfee536241b2e97c8c24e3dc63e04842b5b2ef91b0b91cfa35d88f9cc5b74ce49ed9576a859a0f
6
+ metadata.gz: 24a0b92c12b7f0c2fce898067cbee813d9dbd087697e11a769342f51d8698760aacd2f9d4fc5328641d198e7e5d57f6c4e160a90892eb6cfea760382f8166296
7
+ data.tar.gz: 5c87441340d94046ec3745bc92d20fdb6c4517f0835e7736b45751fc984b616dcfd083f43b887ef0017348511b3432acc08639ec142098a742231b1b7923c14b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.3] - 2026-09-27
9
+
10
+ ### Fixed
11
+
12
+ - `items_by_category` adds an item related to several categories to each of them
13
+ - Before, the item was filed under a merged category such as `["Backend", "Frontend"]`
14
+ - Each category takes its own icon, color and order from the rollups
15
+ - Unchanged empty collections are no longer rewritten on every build
16
+ - An empty collection dumps to a single line (`--- []`) that the previous check missed,
17
+ which made `jekyll serve` regenerate in a loop
18
+ - The fallback keeps data files imported from Notion instead of overwriting them
19
+ - Without `NOTION_TOKEN`, or when a database cannot be fetched, the last Notion data stays in `_data/`
20
+ - Files written by the fallback now say so in their header, and are still regenerated
21
+ - Delete a data file to rebuild it from the Jekyll collection
22
+
23
+ ## [1.0.2] - 2026-01-26
24
+
25
+ ### Fixed
26
+
27
+ - Category order now correctly handles array values from rollup properties
28
+ - Fixes sorting of categories when `Category Order` returns an array instead of a single value
29
+
8
30
  ## [1.0.1] - 2026-01-22
9
31
 
10
32
  ### Fixed
@@ -42,5 +64,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
42
64
 
43
65
  - Secure handling of Notion API tokens via environment variables
44
66
 
67
+ [1.0.2]: https://github.com/maxime-lenne/jekyll-notion-cms/releases/tag/v1.0.2
45
68
  [1.0.1]: https://github.com/maxime-lenne/jekyll-notion-cms/releases/tag/v1.0.1
46
69
  [1.0.0]: https://github.com/maxime-lenne/jekyll-notion-cms/releases/tag/v1.0.0
@@ -214,15 +214,19 @@ notion:
214
214
 
215
215
  Showcase your technical expertise.
216
216
 
217
- **Notion Database Structure:**
217
+ **Notion Database Structure** (`items_by_category` reads these exact property names — see
218
+ [`items_by_category`](#organizer-types) for details):
218
219
 
219
220
  | Property | Type | Description |
220
221
  |----------|------|-------------|
221
222
  | Name | Title | Skill name |
222
- | Category | Select | Skill category (Backend, Frontend, etc.) |
223
- | Level | Select/Number | Proficiency level |
224
- | Icon | Rich text | Icon class or URL |
223
+ | Category | Rollup | Category name, rolled up from a related Categories database |
224
+ | Icon | Rollup | Category icon, rolled up from Categories |
225
+ | Color | Rollup | Skill color |
226
+ | Category Order | Rollup | Category display order, rolled up from Categories |
227
+ | Level | Number | Proficiency level |
225
228
  | Years | Number | Years of experience |
229
+ | Featured | Checkbox | Highlight this skill |
226
230
  | Order | Number | Display order within category |
227
231
 
228
232
  **Configuration:**
@@ -234,15 +238,6 @@ notion:
234
238
  database_env: NOTION_SKILLS_DB
235
239
  data_file: notion_skills.yml
236
240
  organizer: items_by_category
237
- properties:
238
- - { name: Name, type: title }
239
- - { name: Category, type: rollup }
240
- - { name: Level, type: number }
241
- - { name: Icon, type: rich_text }
242
- - { name: Years, type: number }
243
- - { name: Order, type: number }
244
- - { name: Category Icon, type: rollup, key: category_icon }
245
- - { name: Category Order, type: rollup, key: category_order }
246
241
  ```
247
242
 
248
243
  **Template Usage:**
@@ -256,8 +251,11 @@ notion:
256
251
  </h3>
257
252
  <div class="skills-grid">
258
253
  {% for item in category[1].items %}
259
- <div class="skill">
260
- <span class="skill-name">{{ item.name }}</span>
254
+ <div class="skill" style="--skill-color: {{ item.color }}">
255
+ <span class="skill-name">
256
+ {{ item.name }}
257
+ {% if item.featured %}<span class="badge">★</span>{% endif %}
258
+ </span>
261
259
  <div class="skill-bar">
262
260
  <div class="skill-level" style="width: {{ item.level }}%"></div>
263
261
  </div>
@@ -305,16 +303,43 @@ Groups items by their category. Useful for skills, products, team members, or an
305
303
  organizer: items_by_category
306
304
  ```
307
305
 
306
+ > **Note:** unlike the other organizers, `items_by_category` does **not** use the `properties:`
307
+ > list — it reads a fixed set of property names directly from each page. Your database must use
308
+ > these exact names:
309
+ >
310
+ > | Property | Type | Used for |
311
+ > |----------|------|----------|
312
+ > | `Name` | Title | Item name |
313
+ > | `Category` | Rollup | Category name (groups items together) |
314
+ > | `Icon` | Rollup | Category icon |
315
+ > | `Color` | Rollup | Item color |
316
+ > | `Category Order` | Rollup | Category display order |
317
+ > | `Level` | Number | Item level |
318
+ > | `Years` | Number | Item years |
319
+ > | `Featured` | Checkbox | Item featured flag |
320
+ > | `Order` | Number | Item display order within its category |
321
+ >
322
+ > `Category`, `Icon` and `Category Order` are typically rollups from a *Categories* database via
323
+ > a relation field, so all items in the same category share the same icon/order.
324
+
308
325
  Output structure:
309
326
  ```yaml
310
327
  Backend:
311
328
  title: Backend
329
+ category: Backend
330
+ subcategory: null
312
331
  icon: code
313
332
  order: 1
314
333
  items:
315
334
  - name: Ruby
316
335
  level: 90
317
336
  years: 10
337
+ description: null
338
+ icon: null
339
+ color: blue
340
+ featured: true
341
+ order: 1
342
+ id: abc123
318
343
  ```
319
344
 
320
345
  #### `grouped_by`
data/docs/TASKS.md ADDED
@@ -0,0 +1,26 @@
1
+ # Tasks
2
+
3
+ ## To do
4
+
5
+ ### Rollup with a single value returns a scalar instead of an array
6
+
7
+ `PropertyExtractors.extract_rollup_array` returns the value alone when a rollup has one item, and an
8
+ array otherwise (since 1.0.1). A property meant to be a list, such as the `Skills` of an experience,
9
+ is then a string for items with a single skill.
10
+
11
+ Templates that iterate with Liquid do not notice, but anything that expects an array breaks: for
12
+ example `{{ experience.skills | jsonify }}` followed by `.map()` in JavaScript fails with
13
+ `skills.slice(...).map is not a function`.
14
+
15
+ **Proposal (1.1.0):** add an opt-in option on the property, so existing sites keep their behavior:
16
+
17
+ ```yaml
18
+ properties:
19
+ - { name: Skills, type: rollup, array: true }
20
+ ```
21
+
22
+ With `array: true`, the extractor always returns an array: `[]` when empty, `[value]` for a single
23
+ value. Document it in the README and `docs/EXAMPLES_AND_CONFIGURATION.md`, and cover the three cases
24
+ (empty, one value, several values) in `property_extractors_spec.rb`.
25
+
26
+ A default change (always an array) would be a breaking change and belongs to 2.0.
@@ -62,51 +62,99 @@ module JekyllNotionCMS
62
62
  items_by_category = {}
63
63
 
64
64
  notion_data['results'].each do |page|
65
- properties = page['properties']
66
-
67
- name = PropertyExtractors.extract(properties, 'Name', 'title')
68
- next if name.nil? || name.empty?
69
-
70
- level = PropertyExtractors.extract(properties, 'Level', 'number')
71
- years = PropertyExtractors.extract(properties, 'Years', 'number')
72
- featured = PropertyExtractors.extract(properties, 'Featured', 'checkbox')
73
- order = PropertyExtractors.extract(properties, 'Order', 'number')
74
- category_name = PropertyExtractors.extract(properties, 'Category', 'rollup') || 'Other'
75
- category_icon = PropertyExtractors.extract(properties, 'Icon', 'rollup')
76
- category_color = PropertyExtractors.extract(properties, 'Color', 'rollup')
77
- category_order = PropertyExtractors.extract(properties, 'Category Order', 'rollup')
78
-
79
- items_by_category[category_name] ||= {
80
- 'title' => category_name,
81
- 'category' => category_name,
82
- 'subcategory' => nil,
83
- 'icon' => category_icon,
84
- 'order' => category_order || 999,
85
- 'items' => []
86
- }
65
+ process_category_page(page, items_by_category)
66
+ end
67
+
68
+ sort_categories_and_items(items_by_category)
69
+ end
70
+
71
+ # Process a single page for category organization
72
+ def process_category_page(page, items_by_category)
73
+ properties = page['properties']
74
+ name = PropertyExtractors.extract(properties, 'Name', 'title')
75
+ return if name.nil? || name.empty?
87
76
 
88
- items_by_category[category_name]['items'] << {
89
- 'name' => name,
90
- 'level' => level,
91
- 'years' => years,
92
- 'description' => nil,
93
- 'icon' => nil,
94
- 'color' => category_color,
95
- 'featured' => featured,
96
- 'order' => order || 999,
97
- 'id' => page['id']
77
+ split_categories(extract_category_data(properties)).each do |category_data|
78
+ category_name = category_data[:name]
79
+
80
+ items_by_category[category_name] ||= build_category_hash(category_data)
81
+ items_by_category[category_name]['items'] << build_category_item(page, properties, name, category_data)
82
+ end
83
+ end
84
+
85
+ # An item related to several categories gets arrays from its rollups.
86
+ # Split them into one entry per category, pairing each name with its own icon, color and order.
87
+ def split_categories(category_data)
88
+ names = category_data[:name]
89
+ return [category_data] unless names.is_a?(Array) && names.size > 1
90
+
91
+ names.each_with_index.map do |category_name, index|
92
+ {
93
+ name: category_name,
94
+ icon: value_for_category(category_data[:icon], index, names.size),
95
+ color: value_for_category(category_data[:color], index, names.size),
96
+ order: value_for_category(category_data[:order], index, names.size)
98
97
  }
99
98
  end
99
+ end
100
+
101
+ # Rollups drop empty values, so an array only lines up with the categories when the sizes match
102
+ def value_for_category(value, index, category_count)
103
+ return value unless value.is_a?(Array)
104
+
105
+ value.size == category_count ? value[index] : nil
106
+ end
107
+
108
+ # Extract category-related data from properties
109
+ def extract_category_data(properties)
110
+ {
111
+ name: PropertyExtractors.extract(properties, 'Category', 'rollup') || 'Other',
112
+ icon: PropertyExtractors.extract(properties, 'Icon', 'rollup'),
113
+ color: PropertyExtractors.extract(properties, 'Color', 'rollup'),
114
+ order: PropertyExtractors.extract(properties, 'Category Order', 'rollup')
115
+ }
116
+ end
117
+
118
+ # Build the category hash structure
119
+ def build_category_hash(category_data)
120
+ order = category_data[:order]
121
+ {
122
+ 'title' => category_data[:name],
123
+ 'category' => category_data[:name],
124
+ 'subcategory' => nil,
125
+ 'icon' => category_data[:icon],
126
+ 'order' => (order.is_a?(Array) ? order.first : order) || 999,
127
+ 'items' => []
128
+ }
129
+ end
130
+
131
+ # Build an item hash for category organization
132
+ def build_category_item(page, properties, name, category_data)
133
+ {
134
+ 'name' => name,
135
+ 'level' => PropertyExtractors.extract(properties, 'Level', 'number'),
136
+ 'years' => PropertyExtractors.extract(properties, 'Years', 'number'),
137
+ 'description' => nil,
138
+ 'icon' => nil,
139
+ 'color' => category_data[:color],
140
+ 'featured' => PropertyExtractors.extract(properties, 'Featured', 'checkbox'),
141
+ 'order' => PropertyExtractors.extract(properties, 'Order', 'number') || 999,
142
+ 'id' => page['id']
143
+ }
144
+ end
100
145
 
101
- # Sort categories by order
102
- items_by_category = items_by_category.sort_by { |_, data| data['order'].to_i }.to_h
146
+ # Sort categories and their items
147
+ def sort_categories_and_items(items_by_category)
148
+ sorted = items_by_category.sort_by do |_, data|
149
+ order = data['order']
150
+ (order.is_a?(Array) ? order.first : order).to_i
151
+ end.to_h
103
152
 
104
- # Sort items within each category
105
- items_by_category.each_value do |data|
153
+ sorted.each_value do |data|
106
154
  data['items'].sort_by! { |item| item['order'].to_i }
107
155
  end
108
156
 
109
- items_by_category
157
+ sorted
110
158
  end
111
159
 
112
160
  # Organize items grouped by a field
@@ -9,6 +9,9 @@ module JekyllNotionCMS
9
9
  safe true
10
10
  priority :highest
11
11
 
12
+ NOTION_HEADER = 'data imported from Notion'
13
+ FALLBACK_HEADER = 'data generated from the Jekyll collection (Notion fallback)'
14
+
12
15
  def generate(site)
13
16
  @site = site
14
17
  @config = site.config['notion'] || {}
@@ -78,28 +81,22 @@ module JekyllNotionCMS
78
81
  end
79
82
 
80
83
  # Create a YAML data file
81
- def create_data_file(data, file_name, collection_name)
84
+ # @param source [Symbol] :notion or :fallback, recorded in the header
85
+ def create_data_file(data, file_name, collection_name, source: :notion)
82
86
  data_dir = File.join(@site.source, '_data')
83
87
  FileUtils.mkdir_p(data_dir)
84
88
 
85
89
  data_file = File.join(data_dir, file_name)
86
90
  new_content = data.to_yaml
87
91
 
88
- # Skip if content unchanged
89
- if File.exist?(data_file)
90
- existing_content = File.read(data_file)
91
- yaml_start = existing_content.index("---\n")
92
- if yaml_start
93
- existing_yaml = existing_content[yaml_start..]
94
- if existing_yaml.strip == new_content.strip
95
- Jekyll.logger.info 'NotionCMS:', "#{collection_name} data unchanged, skipping"
96
- return
97
- end
98
- end
92
+ if File.exist?(data_file) && yaml_body(File.read(data_file)) == new_content.strip
93
+ Jekyll.logger.info 'NotionCMS:', "#{collection_name} data unchanged, skipping"
94
+ return
99
95
  end
100
96
 
97
+ header = source == :fallback ? FALLBACK_HEADER : NOTION_HEADER
101
98
  File.open(data_file, 'w') do |file|
102
- file.write("# #{collection_name.capitalize} data imported from Notion\n")
99
+ file.write("# #{collection_name.capitalize} #{header}\n")
103
100
  file.write("# Auto-generated by jekyll-notion-cms - Do not edit manually\n")
104
101
  file.write("# Last updated: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}\n\n")
105
102
  file.write(new_content)
@@ -108,6 +105,29 @@ module JekyllNotionCMS
108
105
  Jekyll.logger.info 'NotionCMS:', "#{collection_name} written to _data/#{file_name}"
109
106
  end
110
107
 
108
+ # YAML part of a data file, without the comment header.
109
+ # Works for single-line documents such as "--- []" that an empty collection produces.
110
+ def yaml_body(content)
111
+ content.lines.drop_while { |line| line.start_with?('#') || line.strip.empty? }.join.strip
112
+ end
113
+
114
+ # Without Notion, keep the last data imported from it: Jekyll already loaded the file into site.data
115
+ def keep_notion_data?(data_file, collection_name)
116
+ return false unless notion_data_file?(File.join(@site.source, '_data', data_file))
117
+
118
+ Jekyll.logger.info 'NotionCMS:', "Keeping Notion data in _data/#{data_file} for #{collection_name}"
119
+ true
120
+ end
121
+
122
+ # True when the file holds data imported from Notion rather than a previous fallback.
123
+ # Older versions wrote the Notion header for fallback data too, so also look for collection ids.
124
+ def notion_data_file?(data_file)
125
+ return false unless File.exist?(data_file)
126
+
127
+ content = File.read(data_file)
128
+ content.lines.first.to_s.include?(NOTION_HEADER) && !content.match?(/\bid: collection_\d+\b/)
129
+ end
130
+
111
131
  # Use fallback for all collections
112
132
  def use_all_collections_fallback
113
133
  @collections_config.each do |collection_name, config|
@@ -120,6 +140,9 @@ module JekyllNotionCMS
120
140
  Jekyll.logger.info 'NotionCMS:', "Using fallback for #{collection_name}"
121
141
 
122
142
  data_file = config['data_file']
143
+
144
+ return if keep_notion_data?(data_file, collection_name)
145
+
123
146
  data_key = data_file.sub('.yml', '').sub('.yaml', '')
124
147
  properties_config = config['properties'] || []
125
148
 
@@ -137,7 +160,7 @@ module JekyllNotionCMS
137
160
 
138
161
  organized_data = DataOrganizers.organize(mock_data, config)
139
162
  @site.data[data_key] = organized_data
140
- create_data_file(organized_data, data_file, collection_name)
163
+ create_data_file(organized_data, data_file, collection_name, source: :fallback)
141
164
 
142
165
  count = organized_data.is_a?(Hash) ? organized_data.size : organized_data.length
143
166
  Jekyll.logger.info 'NotionCMS:', "#{collection_name} fallback applied (#{count} items)"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JekyllNotionCMS
4
- VERSION = '1.0.1'
4
+ VERSION = '1.0.3'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-notion-cms
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maxime Lenne
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-22 00:00:00.000000000 Z
11
+ date: 2026-09-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -146,10 +146,9 @@ files:
146
146
  - Rakefile
147
147
  - docs/Download_on_the_App_Store_Badge_FR_RGB_wht_100217.svg
148
148
  - docs/EXAMPLES_AND_CONFIGURATION.md
149
+ - docs/TASKS.md
149
150
  - docs/architecture.excalidraw
150
151
  - docs/architecture.png
151
- - docs/templates/architecture.excalidraw
152
- - docs/templates/architecture.png
153
152
  - docs/templates/github-actions_notion-sync.yml
154
153
  - docs/templates/n8n-workflow_Notion-database-change-trigger-GitHub-Actions.json
155
154
  - lib/jekyll-notion-cms.rb