jekyll-ai-related 0.2.1 → 0.3.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: d8d5e15d03e0c8ed77ab9de0b6a6955e7ab78fee435529ade1dc1dfa9dc1679c
4
- data.tar.gz: 6486621d77107539b5ae32af1bb2376f02b2f59f7074dc0a4ded1650c3db6d78
3
+ metadata.gz: '03779eb5ce75f69231a8a7563008c2ebe851bc21ef619763d42e119045830b1d'
4
+ data.tar.gz: 4f28fba97390ff1497dfe5309eec9cee958f9148356bcf5673613522fdd6a1ab
5
5
  SHA512:
6
- metadata.gz: b10bdbef5642400506c067400ab7900a85611fe1c297a20a495a81051f258cacf727e19834884b43a6978009f795ca435d7f18189484bc3a361f9d372a1ae615
7
- data.tar.gz: 4e8112831d3f36bfcb194613b791d404f8d9df1d3ad7254f1785b4834f0b42b3268d1a74b51aef9b67f2122ae0e6999236cf1f250491f9f1050663115faefd8a
6
+ metadata.gz: 91b81a9b34325df10bd2d7b649b5e0237590f2a1211e6e3f65307a1019826c4eb8702f90628d7ad7cc5f2b68f4abe2e531f73dd08e0b24d656ae980bfffd3558
7
+ data.tar.gz: 6e84604c67c77b0e7373f352bc0df6aa320b99ba160e186b3dd8f17ef43c879c8a65ad55c11ffce78b6504b1467448b77bc2ca245d1f63c7a7a79bdc0f882853
@@ -14,7 +14,7 @@ jobs:
14
14
  runs-on: ubuntu-latest
15
15
  steps:
16
16
  - name: Create release and upload assets
17
- uses: softprops/action-gh-release@v2
17
+ uses: softprops/action-gh-release@v3
18
18
  with:
19
19
  name: ${{ github.ref_name }}
20
20
  tag_name: ${{ github.ref_name }}
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jekyll-ai-related (0.2.1)
4
+ jekyll-ai-related (0.3.0)
5
5
  httparty (~> 0.22)
6
6
  jekyll (>= 3.7, < 5.0)
7
7
  json (~> 2.7)
@@ -9,33 +9,33 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- addressable (2.8.8)
12
+ addressable (2.9.0)
13
13
  public_suffix (>= 2.0.2, < 8.0)
14
14
  ast (2.4.3)
15
15
  base64 (0.3.0)
16
- bigdecimal (4.0.1)
16
+ bigdecimal (4.1.2)
17
17
  colorator (1.1.0)
18
- concurrent-ruby (1.3.6)
19
- csv (3.3.5)
18
+ concurrent-ruby (1.3.8)
19
+ csv (3.3.6)
20
20
  em-websocket (0.5.3)
21
21
  eventmachine (>= 0.12.9)
22
22
  http_parser.rb (~> 0)
23
23
  eventmachine (1.2.7)
24
- ffi (1.17.3)
25
- ffi (1.17.3-x86_64-linux-gnu)
24
+ ffi (1.17.4)
25
+ ffi (1.17.4-x86_64-linux-gnu)
26
26
  forwardable-extended (2.6.0)
27
- google-protobuf (4.33.5)
27
+ google-protobuf (4.35.1)
28
28
  bigdecimal
29
- rake (>= 13)
30
- google-protobuf (4.33.5-x86_64-linux-gnu)
29
+ rake (~> 13.3)
30
+ google-protobuf (4.35.1-x86_64-linux-gnu)
31
31
  bigdecimal
32
- rake (>= 13)
32
+ rake (~> 13.3)
33
33
  http_parser.rb (0.8.1)
34
34
  httparty (0.24.2)
35
35
  csv
36
36
  mini_mime (>= 1.0.0)
37
37
  multi_xml (>= 0.5.2)
38
- i18n (1.14.8)
38
+ i18n (1.15.2)
39
39
  concurrent-ruby (~> 1.0)
40
40
  jekyll (4.4.1)
41
41
  addressable (~> 2.4)
@@ -60,12 +60,12 @@ GEM
60
60
  sass-embedded (~> 1.75)
61
61
  jekyll-watch (2.2.1)
62
62
  listen (~> 3.0)
63
- json (2.18.1)
63
+ json (2.21.1)
64
64
  kramdown (2.5.2)
65
65
  rexml (>= 3.4.4)
66
66
  kramdown-parser-gfm (1.1.0)
67
67
  kramdown (~> 2.0)
68
- language_server-protocol (3.17.0.5)
68
+ language_server-protocol (3.17.0.6)
69
69
  liquid (4.0.4)
70
70
  listen (3.10.0)
71
71
  logger
@@ -74,23 +74,24 @@ GEM
74
74
  logger (1.7.0)
75
75
  mercenary (0.4.0)
76
76
  mini_mime (1.1.5)
77
- multi_xml (0.8.1)
77
+ minitest (5.27.0)
78
+ multi_xml (0.9.1)
78
79
  bigdecimal (>= 3.1, < 5)
79
- parallel (1.27.0)
80
- parser (3.3.10.1)
80
+ parallel (1.28.0)
81
+ parser (3.3.12.0)
81
82
  ast (~> 2.4.1)
82
83
  racc
83
84
  pathutil (0.16.2)
84
85
  forwardable-extended (~> 2.6)
85
86
  prism (1.9.0)
86
- public_suffix (7.0.2)
87
+ public_suffix (7.0.5)
87
88
  racc (1.8.1)
88
89
  rainbow (3.1.1)
89
- rake (13.3.1)
90
+ rake (13.4.2)
90
91
  rb-fsevent (0.11.2)
91
92
  rb-inotify (0.11.1)
92
93
  ffi (~> 1.0)
93
- regexp_parser (2.11.3)
94
+ regexp_parser (2.12.0)
94
95
  rexml (3.4.4)
95
96
  rouge (4.7.0)
96
97
  rubocop (1.57.2)
@@ -104,7 +105,7 @@ GEM
104
105
  rubocop-ast (>= 1.28.1, < 2.0)
105
106
  ruby-progressbar (~> 1.7)
106
107
  unicode-display_width (>= 2.4.0, < 3.0)
107
- rubocop-ast (1.49.0)
108
+ rubocop-ast (1.50.0)
108
109
  parser (>= 3.3.7.2)
109
110
  prism (~> 1.7)
110
111
  rubocop-jekyll (0.14.0)
@@ -115,10 +116,10 @@ GEM
115
116
  rubocop-ast (>= 1.31.1, < 2.0)
116
117
  ruby-progressbar (1.13.0)
117
118
  safe_yaml (1.0.5)
118
- sass-embedded (1.97.3)
119
+ sass-embedded (1.102.0)
119
120
  google-protobuf (~> 4.31)
120
121
  rake (>= 13)
121
- sass-embedded (1.97.3-x86_64-linux-gnu)
122
+ sass-embedded (1.102.0-x86_64-linux-gnu)
122
123
  google-protobuf (~> 4.31)
123
124
  terminal-table (3.0.2)
124
125
  unicode-display_width (>= 1.1.1, < 3)
@@ -132,6 +133,7 @@ PLATFORMS
132
133
  DEPENDENCIES
133
134
  bundler (~> 2.6)
134
135
  jekyll-ai-related!
136
+ minitest (~> 5.25)
135
137
  rake (~> 13.0)
136
138
  rubocop-jekyll (~> 0.14)
137
139
 
data/MIGRATE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Migrate
2
2
 
3
- Migration instructions when breaking changes are introduced.
3
+ This file is the compatibility entry point for migration guidance. See the complete [migration and rebuild guide](docs/MIGRATIONS.md) for current procedures.
4
4
 
5
5
  ## From 0.1.0 to 0.2.0
6
6
 
@@ -18,6 +18,4 @@ If you are upgrading from `v0.1.0`, you need drop the data and re-run the plugin
18
18
 
19
19
  ### Environments support
20
20
 
21
- This version also introduces the concept of *environments*. It reads `JEKYLL_ENV` env var to allow you to have different tables and functions for different environments.
22
-
23
- You need to set `JEKYLL_ENV` every time you run the plugin to use this feature. It can be set to anything, like `ci`, `dev`, or `production`. Not setting the env var will make the plugin behave like version `0.1.0`.
21
+ This version also introduced environment-specific table and function names through `JEKYLL_ENV`. See the [environment migration guidance](docs/MIGRATIONS.md#environment-migration) for setup and safety details.
data/README.md CHANGED
@@ -4,62 +4,52 @@
4
4
  [![GitHub Release](https://img.shields.io/github/v/release/pirafrank/jekyll-ai-related)](https://github.com/pirafrank/jekyll-ai-related/releases/latest)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
- A [Jekyll](https://jekyllrb.com/) [command](https://jekyllrb.com/docs/plugins/commands/) plugin to generate list of related posts using AI.
7
+ A [Jekyll](https://jekyllrb.com/) command plugin that uses OpenAI embeddings and Supabase/pgvector to generate related-post data for your blog.
8
8
 
9
- The plugin uses uses OpenAI API to generate embeddings from posts content, and stores them on Supabase vector database. A query on Supabase provides a similarity score between the post and all other posts on the site. The plugin then selects the most similar posts to generate the list of related posts.
9
+ The plugin writes related-post results to `_data`, so normal `jekyll build` runs do not call OpenAI or Supabase. Run `jekyll related` when posts are added or changed, or run it in your CI pipeline.
10
10
 
11
- To avoid unnecessary API calls or slowing down the website generation, the plugin stores the list of related posts in the site's `_data` folder. This way you can `jekyll build` without calling the plugin or making API calls, while still having the list of related posts available as site data. You only need to re-run it when you add or update posts.
11
+ ## Quick start
12
12
 
13
- ## Getting started
13
+ ### Requirements
14
14
 
15
- 1. Setup Supabase
16
- 2. Install the plugin
17
- 3. Configure the plugin
18
- 4. Run the plugin
15
+ - Ruby `>= 3.2`
16
+ - Jekyll `>= 3.7` and `< 5.0`
17
+ - An OpenAI API key
18
+ - A Supabase project with pgvector available
19
19
 
20
- ## Requirements
20
+ ### 1. Create the database objects
21
21
 
22
- - OpenAI account and API key
23
- - Supabase account and API key
24
- - Supabase URL of the project where the DB is located
22
+ Run [`sql/supabase/create.sql`](sql/supabase/create.sql) in the Supabase SQL editor. This creates the default `page_embeddings` table and `cosine_similarity` function.
25
23
 
26
- ### Supabase setup
24
+ For existing installations, follow the [migration guide](docs/MIGRATIONS.md), especially the v0.3.0 `embedding_fingerprint` column migration. See [Supabase setup](docs/SUPABASE.md) for environment-specific tables and maintenance.
27
25
 
28
- Use the `create.sql` script in the `sql\supabase` folder of this repo to create the required table and functions on your Supabase project. You can run the script in the SQL editor of the Supabase dashboard.
26
+ ### 2. Install the plugin
29
27
 
30
- You can always revert the changes by running the `drop.sql` script.
28
+ Add the gem to the Jekyll site's `Gemfile`:
31
29
 
32
- ## Installation
33
-
34
- 1. Add the plugin to you Jekyll site's `Gemfile` in the `:jekyll_plugins` group:
35
-
36
- ```Gemfile
30
+ ```ruby
37
31
  group :jekyll_plugins do
38
- gem 'jekyll-ai-related'
32
+ gem "jekyll-ai-related"
39
33
  end
40
34
  ```
41
35
 
42
- 2. Run `bundle install`
36
+ Then install it:
43
37
 
44
- ### Install from git
38
+ ```sh
39
+ bundle install
40
+ ```
45
41
 
46
- Alternatively, you can get code straight from this repository. Code from `main` branch should be stable enough but may contain unreleased software with bugs or breaking changes. Unreleased software should be considered of beta quality.
42
+ To use the unreleased `main` branch instead:
47
43
 
48
- ```Gemfile
44
+ ```ruby
49
45
  group :jekyll_plugins do
50
- gem 'jekyll-ai-related', git: 'https://github.com/pirafrank/jekyll-ai-related', branch: 'main'
46
+ gem "jekyll-ai-related", git: "https://github.com/pirafrank/jekyll-ai-related", branch: "main"
51
47
  end
52
48
  ```
53
49
 
54
- ## Update
50
+ ### 3. Configure the site
55
51
 
56
- ```sh
57
- bundle update jekyll-ai-related
58
- ```
59
-
60
- ## Configuration
61
-
62
- Sample configuration to add to your `_config.yml`:
52
+ Add the plugin configuration to `_config.yml`:
63
53
 
64
54
  ```yaml
65
55
  jekyll-ai-related:
@@ -75,153 +65,74 @@ jekyll-ai-related:
75
65
  db_function: cosine_similarity
76
66
  ```
77
67
 
78
- Configuration is optional. The plugin will use the default values if not provided.
79
-
80
- ### Configuration options
81
-
82
- | Option | Type | Description | Default Value |
83
- | --- | --- | --- | --- |
84
- | `post_unique_field` | string | A field that uniquely identifies a post | `slug` |
85
- | `post_updated_field` | string | A field that indicates when the post was last updated | `date` |
86
- | `output_path` | string | The subdir inside `_data` where related posts will be stored | `related_posts` |
87
- | `include_drafts` | boolean | Whether to include drafts in the list of related posts | `false` |
88
- | `include_future` | boolean | Whether to include future posts in the list of related posts | `false` |
89
- | `related_posts_limit` | integer | The maximum number of related posts to extract per post | `3` |
90
- | `related_posts_score_threshold` | float | The minimum similarity score to consider a post related | `0.5` |
91
- | `precision` | integer | The number of decimal digits to round similarity scores to | `3` |
92
- | `db_table` | string | The name of the table where the embeddings are stored | `page_embeddings` |
93
- | `db_function` | string | The name of the function to calculate similarity scores | `cosine_similarity` |
94
-
95
- > [!NOTE]
96
- > `related_posts_limit` and `related_posts_score_threshold` are used to filter the list of related posts. The plugin will return the top `related_posts_limit` posts with a similarity score greater than `related_posts_score_threshold`. A post may have 0 or more than `related_posts_limit` related posts, but only the top `related_posts_limit` will be returned, if any.
97
-
98
- ### Advanced configuration
99
-
100
- You can customize the unique and updated fields, for example if you have a plugin that generates a unique ID for each post, or if you have a custom field that indicates when the post was last updated.
101
-
102
- Check the [post-metadata.rb](https://github.com/pirafrank/fpiracom/blob/8cc17a5801a73f7c8cbad4cbee099db18389b187/_plugins/post-metadata.rb) plugin of mine for an example. You can download and place it in your `_plugins` folder. It will add metadata to each `post` object and makes it accessible anywhere the `post` object is.
103
-
104
- If you do so, you could use the following configuration:
105
-
106
- ```yaml
107
- jekyll-ai-related:
108
- post_unique_field: uid
109
- post_updated_field: most_recent_edit
110
- ```
111
-
112
- ## Usage
113
-
114
- To run, the plugin needs the OpenAI and Supabase API keys, and Supabase URL. The plugin expects them as environment variables.
68
+ These defaults are optional. Set the required credentials in the environment:
115
69
 
116
70
  ```sh
117
71
  export OPENAI_API_KEY="your_openai_api_key"
118
- export SUPABASE_URL="your_supabase_url"
72
+ export SUPABASE_URL="https://your-project.supabase.co"
119
73
  export SUPABASE_KEY="your_supabase_api_key"
120
74
  ```
121
75
 
122
- Never write API keys in your code or configuration files you commit.
76
+ Never commit API keys. See the [configuration reference](docs/CONFIGURATION.md) for all settings and custom post fields.
123
77
 
124
- Then you can run the plugin with:
78
+ ### 4. Generate related posts
125
79
 
126
80
  ```sh
127
81
  bundle exec jekyll related
128
82
  ```
129
83
 
130
- > [!IMPORTANT]
131
- > Re-run the plugin whenever you add or update posts to update the list of related posts. Only posts with a `post_updated_field` date greater than the last run will be processed.
132
-
133
- ### Options
134
-
135
- You can pass options to the plugin to change its behavior. Options are passed after the command name. Not specifying an option is the same as specifying the default value.
136
-
137
- | Option | Description | Default |
138
- | --- | --- | --- |
139
- | `--debug` | Most verbose. Sets log level to Debug. | Jekyll default |
140
- | `--quiet` | Do not print Info logs. Sets log level to Error. | Jekyll default |
141
- | `--future` | Generate embeddings and find related posts also for those with a future date. | `false` |
142
- | `--drafts` | Generate embeddings and find related posts also for drafts. | `false` |
143
- | `--dry-run` | Do not update the database, do not write related posts to disk. | `false` |
144
-
145
- Example:
84
+ A useful validation run is:
146
85
 
147
86
  ```sh
148
- bundle exec jekyll related --dry-run --future
87
+ bundle exec jekyll related --dry-run --debug
149
88
  ```
150
89
 
151
- ### Environments
152
-
153
- Jekyll AI Related can optionally use the `JEKYLL_ENV` environment variable to determine where is running. This allows you to have separate tables and functions for different environments, for example for development and production.
154
-
155
- When `JEKYLL_ENV` is set, the plugin will append its value to `db_table` and `db_function` configuration options, separated by a `_` character. If it is not set, the plugin won't append anything. You must set `JEKYLL_ENV` every time you run the plugin to use environments.
156
-
157
- > [!IMPORTANT]
158
- > You need to make sure you have the corresponding tables and functions on your Supabase project. Edit the committed SQL script accordingly. They work with no modification when `JEKYLL_ENV` is not set.
90
+ Dry-run mode does not update Supabase or write YAML files, but it may call OpenAI for cache misses. Use a normal run to populate the database and generate output.
159
91
 
160
- For example, if you `export JEKYLL_ENV=production` and have the configuration below
92
+ Generated data is written under `_data/related_posts/`, for example:
161
93
 
162
- ```yaml
163
- jekyll-ai-related:
164
- db_table: cool_table_name
165
- db_function: cool_function_name
94
+ ```text
95
+ _data/related_posts/my-post.yml
166
96
  ```
167
97
 
168
- the plugin will use:
169
-
170
- - `cool_table_name_production` as the database table name,
171
- - `cool_function_name_production` as the database function name.
172
-
173
- > [!TIP]
174
- > Explicitly declare `JEKYLL_ENV=production` to avoid overwriting production data by mistake.
175
-
176
- ## Jekyll integration
98
+ See the [integration guide](docs/INTEGRATION.md) for using the data in Liquid templates.
177
99
 
178
- The plugin generates a list of related posts for each post in the site. Yet it won't edit your posts or layouts to display the list. It's up to you to integrate it in your site.
100
+ ## Environments
179
101
 
180
- You can access the list in the `site.data.YOUR_output_path_NAME` object. For example, with the default settings you can access it with `site.data.related_posts[post.slug]`.
102
+ Set `JEKYLL_ENV` to isolate development, staging, and production data:
181
103
 
182
- Here's a snippet to add it to the bottom of your `_layouts/post.html` layout file. It works with the default configuration.
183
-
184
- ```html
185
- {% if site.data.related_posts[page.slug] %}
186
- <div class="related-posts">
187
- <h3>Related Posts</h3>
188
- <ul>
189
- {% for post in site.data.related_posts[page.slug] %}
190
- <li>
191
- <a href="{{ post.url }}">{{ post.title }}</a>
192
- <br/>{{ post.date | date: "%B %d, %Y" }}
193
- </li>
194
- {% endfor %}
195
- </ul>
196
- </div>
197
- {% endif %}
104
+ ```sh
105
+ JEKYLL_ENV=production bundle exec jekyll related
198
106
  ```
199
107
 
200
- > [!NOTE]
201
- > The plugin writes posts in descending order of similarity score, from most similar to least similar. You can change the order by reversing the loop in the snippet above.
202
-
203
- ## Development
204
-
205
- Clone and run `bundle install` to get started.
108
+ The environment suffix is appended to the configured table and RPC function names, so the corresponding Supabase objects must already exist. See [Supabase setup](docs/SUPABASE.md) for details.
206
109
 
207
- Code lives in `lib/jekyll` directory. `lib/jekyll/commands/generator.rb` is the entry point of the plugin, as per Jekyll documentation. More details [here](https://jekyllrb.com/docs/plugins/commands/).
110
+ ## Documentation
208
111
 
209
- ## Contributing
112
+ The [documentation index](docs/README.md) groups the complete guides by setup, operation, implementation, and development.
210
113
 
211
- [Bug reports](https://github.com/pirafrank/jekyll-ai-related/issues) and [pull requests](https://github.com/pirafrank/jekyll-ai-related/pulls) are welcome on GitHub.
114
+ - [Getting started](docs/GETTING_STARTED.md)
115
+ - [Configuration](docs/CONFIGURATION.md)
116
+ - [Migrations](docs/MIGRATIONS.md)
117
+ - [Workflow and operations](docs/WORKFLOW.md)
118
+ - [Troubleshooting](docs/TROUBLESHOOTING.md)
119
+ - [Architecture and embeddings](docs/ARCHITECTURE.md), [embedding details](docs/EMBEDDINGS.md)
120
+ - [Development](docs/DEVELOPMENT.md)
121
+ - [Security](docs/SECURITY.md)
212
122
 
213
- ## Code of Conduct
214
-
215
- Everyone interacting in the project's codebase, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/pirafrank/jekyll-ai-related/blob/main/CODE_OF_CONDUCT.md).
216
-
217
- ## Guarantee
123
+ ## Development
218
124
 
219
- This plugin is provided as is, without any guarantee. Use at your own risk.
125
+ ```sh
126
+ bundle install
127
+ bundle exec rake test
128
+ bundle exec rake lint
129
+ bundle exec rake build
130
+ ```
220
131
 
221
- ## Disclaimer
132
+ See [Development](docs/DEVELOPMENT.md) for the repository layout, validation workflow, and release process.
222
133
 
223
- This plugin uses OpenAI API and Supabase, and you are responsible for complying with their respective terms of service. This plugin is not affiliated with OpenAI or Supabase.
134
+ ## Contributing and license
224
135
 
225
- ## License
136
+ [Bug reports](https://github.com/pirafrank/jekyll-ai-related/issues) and [pull requests](https://github.com/pirafrank/jekyll-ai-related/pulls) are welcome. The project is available under the [MIT License](https://opensource.org/licenses/MIT).
226
137
 
227
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
138
+ This plugin uses OpenAI and Supabase services. Users are responsible for complying with their respective terms of service. The plugin is not affiliated with either service.
data/Rakefile CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rubocop/rake_task"
5
+ require "rake/testtask"
5
6
 
6
7
  RuboCop::RakeTask.new
7
8
 
@@ -9,6 +10,12 @@ task :default do
9
10
  sh "rake -AT"
10
11
  end
11
12
 
13
+ Rake::TestTask.new(:test) do |t|
14
+ t.libs << "lib"
15
+ t.libs << "test"
16
+ t.pattern = "test/**/*_test.rb"
17
+ end
18
+
12
19
  task lint: :rubocop
13
20
 
14
21
  task :changelog do
@@ -0,0 +1,96 @@
1
+ # Architecture
2
+
3
+ Jekyll AI Related is a Ruby gem that adds the `jekyll related` command to Jekyll. The command reads the site's posts, reuses cached embeddings when possible or generates them through OpenAI, stores vectors and post metadata in Supabase/Postgres, performs a similarity search, and writes the resulting related-post lists into the site's `_data` directory.
4
+
5
+ The generated YAML is deliberately local to the site. A normal `jekyll build` can therefore consume related-post data without calling OpenAI or Supabase; the command is run separately when content changes.
6
+
7
+ ## System boundary
8
+
9
+ ```text
10
+ Jekyll site External services Generated site data
11
+ ----------- ------------------ -------------------
12
+ posts + front matter
13
+ |
14
+ v
15
+ `jekyll related` --> OpenAI embeddings API
16
+ | |
17
+ | v
18
+ +------------> Supabase REST API
19
+ (Postgres + pgvector)
20
+ |
21
+ v
22
+ similarity results
23
+ |
24
+ v
25
+ `_data/<path>/*.yml`
26
+ |
27
+ v
28
+ normal Jekyll templates / `site.data`
29
+ ```
30
+
31
+ The repository contains the gem code under `lib/`, the Supabase bootstrap SQL under `sql/supabase/`, and packaging/release configuration. There are no application controllers, web server, runtime database connections, or test suite in this repository; the plugin runs as a batch command during content maintenance.
32
+
33
+ ## Runtime components
34
+
35
+ - `lib/jekyll-ai-related.rb` loads Jekyll and the processor.
36
+ - `lib/jekyll/commands/generator.rb` registers the `related` Jekyll command and translates CLI flags into options.
37
+ - `lib/jekyll/embeddings-generator/init.rb` builds and validates configuration, then creates and reads a Jekyll site. `site.generate` is called so other Jekyll generator plugins can enrich post data before processing.
38
+ - `lib/jekyll/processor.rb` coordinates the two phases: embedding persistence and related-post generation.
39
+ - `embeddings/generate.rb` calls `https://api.openai.com/v1/embeddings` using the `text-embedding-3-small` model.
40
+ - `embeddings/store.rb` communicates with Supabase using its REST/PostgREST endpoints. It handles existence checks, upserts, vector retrieval, and the similarity RPC call.
41
+ - `models/data.rb` creates the database payload. `models/metadata.rb` creates the JSON metadata stored beside each vector.
42
+ - `sql/supabase/create.sql` enables pgvector, creates the table and IVFFlat cosine index, and defines the stored procedure used by the plugin.
43
+
44
+ ## Command lifecycle
45
+
46
+ 1. Jekyll loads the gem and exposes `bundle exec jekyll related`.
47
+ 2. The command combines `_config.yml`, CLI flags, environment variables, and defaults. It validates `OPENAI_API_KEY`, `SUPABASE_URL`, and `SUPABASE_KEY`.
48
+ 3. A fresh Jekyll site is built with the configured draft and future-post settings. The site is read and generators are run.
49
+ 4. For every included post, the plugin extracts `post.content` and metadata, computes a model/content fingerprint, and fetches the existing Supabase record.
50
+ 5. The plugin reuses the stored embedding when the fingerprint and vector are present; otherwise it calls OpenAI. It upserts when the row is missing, the fingerprint changed, or the configured update value is newer.
51
+ 6. For every included post, it fetches that post's stored embedding and submits a SQL query through the configured Supabase RPC function. The query excludes the current post, applies the score threshold, sorts by cosine distance, and limits the result count.
52
+ 7. Non-empty results are serialized as YAML to `_data/<output_path>/<safe-uid>.yml`. Existing files for the same post are overwritten; an empty result does not remove an older file.
53
+
54
+ The two phases are sequential. If related-post lookup fails for one post, the error is logged and processing continues with the next post.
55
+
56
+ ## Configuration and naming
57
+
58
+ The `jekyll-ai-related` section of `_config.yml` supports:
59
+
60
+ | Setting | Default | Role |
61
+ | --- | --- | --- |
62
+ | `post_unique_field` | `slug` | Post field used as the database `uid` and output filename key |
63
+ | `post_updated_field` | `date` | Post field used for update comparison |
64
+ | `output_path` | `related_posts` | Subdirectory under `_data` |
65
+ | `include_drafts` | `false` | Include drafts in the generated Jekyll site |
66
+ | `include_future` | `false` | Include future-dated posts |
67
+ | `related_posts_limit` | `3` | Maximum result count per post |
68
+ | `related_posts_score_threshold` | `0.5` | Minimum cosine similarity |
69
+ | `precision` | `3` | Decimal places used for returned similarity values |
70
+ | `db_table` | `page_embeddings` | Supabase table name |
71
+ | `db_function` | `cosine_similarity` | Supabase RPC function name |
72
+
73
+ `JEKYLL_ENV`, when present, is appended to both database names with an underscore. For example, `JEKYLL_ENV=production` changes the defaults to `page_embeddings_production` and `cosine_similarity_production`. The corresponding objects must exist in Supabase; the plugin does not create environment-specific objects automatically.
74
+
75
+ CLI flags can enable drafts, future posts, and dry-run mode for a single invocation. `--debug` and `--quiet` adjust Jekyll logging.
76
+
77
+ ## Output contract
78
+
79
+ For the default configuration, a post with slug `my-post` produces:
80
+
81
+ ```text
82
+ _data/related_posts/my-post.yml
83
+ ```
84
+
85
+ The YAML value is an array of result objects returned by Supabase. Each result contains `title`, `uid`, `most_recent_edit`, `url`, `date`, and a rounded `similarity` value. Templates access it as `site.data.related_posts[page.slug]`.
86
+
87
+ The filename is lowercased and every character outside `[a-z0-9-_]` is replaced by `-`. The database lookup still uses the original configured post value, so the filename is only a filesystem-safe representation.
88
+
89
+ ## Operational characteristics
90
+
91
+ - API keys are read only from environment variables and are sent in the OpenAI and Supabase HTTP headers.
92
+ - The command is not part of the normal build path. Keep generated `_data` files available to the site build, and schedule `jekyll related` after creating or editing posts.
93
+ - OpenAI is called only for cache misses: new posts, changed rendered content, legacy rows without a fingerprint, or after an embedding model change. Supabase reads and similarity searches still run for every included post.
94
+ - `--dry-run` still reads from both services and may generate embeddings for cache misses; it skips Supabase updates and local file writes. It is therefore useful for exercising the flow, but it is not an offline mode.
95
+ - The included SQL uses an IVFFlat index with `vector_cosine_ops` and `lists = 100`. The vector dimension must remain compatible with the selected OpenAI model and the table definition.
96
+ - The plugin uses raw SQL text assembled in Ruby and executes it through a `SECURITY DEFINER` PostgreSQL function. Table/function names and the SQL setup should be treated as trusted deployment configuration.
@@ -0,0 +1,81 @@
1
+ # Configuration reference
2
+
3
+ Configuration is read from the `jekyll-ai-related` section of `_config.yml`.
4
+
5
+ ```yaml
6
+ jekyll-ai-related:
7
+ post_unique_field: slug
8
+ post_updated_field: date
9
+ output_path: related_posts
10
+ include_drafts: false
11
+ include_future: false
12
+ related_posts_limit: 3
13
+ related_posts_score_threshold: 0.5
14
+ precision: 3
15
+ db_table: page_embeddings
16
+ db_function: cosine_similarity
17
+ ```
18
+
19
+ | Setting | Type | Default | Description |
20
+ | --- | --- | --- | --- |
21
+ | `post_unique_field` | string | `slug` | Post field used as the database `uid` and output filename key. |
22
+ | `post_updated_field` | string | `date` | Post field compared with the stored `most_recent_edit`. |
23
+ | `output_path` | string | `related_posts` | Directory under `_data` for generated YAML. |
24
+ | `include_drafts` | boolean | `false` | Include drafts when building the processing site. |
25
+ | `include_future` | boolean | `false` | Include future-dated posts. |
26
+ | `related_posts_limit` | integer | `3` | Maximum number of results per source post. |
27
+ | `related_posts_score_threshold` | number | `0.5` | Minimum cosine similarity. |
28
+ | `precision` | integer | `3` | Decimal places used in returned similarity values. |
29
+ | `db_table` | string | `page_embeddings` | Supabase table containing vectors. |
30
+ | `db_function` | string | `cosine_similarity` | Supabase RPC function used for the search. |
31
+
32
+ > [!NOTE]
33
+ > `related_posts_limit` and `related_posts_score_threshold` work together: the plugin returns up to `related_posts_limit` posts whose similarity is greater than `related_posts_score_threshold`. A post can have no qualifying related posts, and the threshold can result in fewer results than the limit.
34
+
35
+ CLI flags are invocation-level overrides for drafts, future posts, and dry run:
36
+
37
+ ```text
38
+ --drafts include drafts
39
+ --future include future posts
40
+ --dry-run skip database updates and file writes
41
+ --debug enable debug logging
42
+ --quiet suppress info logging
43
+ ```
44
+
45
+ For drafts and future posts, a true value in `_config.yml` or the corresponding CLI flag enables inclusion. `--dry-run` is not an offline mode.
46
+
47
+ ## Environment naming
48
+
49
+ If `JEKYLL_ENV` is set, its value is appended to both database names:
50
+
51
+ ```sh
52
+ JEKYLL_ENV=production
53
+ ```
54
+
55
+ With the defaults, the plugin uses `page_embeddings_production` and `cosine_similarity_production`. The suffixed objects must already exist.
56
+
57
+ ## Credentials
58
+
59
+ Credentials are not Jekyll configuration values. They are required environment variables:
60
+
61
+ - `OPENAI_API_KEY`
62
+ - `SUPABASE_URL`
63
+ - `SUPABASE_KEY`
64
+
65
+ Missing credentials stop configuration validation before processing posts.
66
+
67
+ ## Precedence and important behavior
68
+
69
+ Configuration values come from `_config.yml` and defaults; CLI flags can enable drafts, future posts, and dry run for the current command. `post_unique_field` and `post_updated_field` must exist on every processed post. The plugin computes a content fingerprint before calling OpenAI and reuses stored vectors when the fingerprint matches. Metadata-only changes still trigger a database upsert when the configured update timestamp is newer than the stored value.
70
+
71
+ ## Advanced custom fields
72
+
73
+ Sites can use custom post metadata for identity and freshness. For example, a plugin that adds a unique `uid` and a `most_recent_edit` field can use:
74
+
75
+ ```yaml
76
+ jekyll-ai-related:
77
+ post_unique_field: uid
78
+ post_updated_field: most_recent_edit
79
+ ```
80
+
81
+ The fields must be available on the post after Jekyll's generators have run. `uid` must be unique across the configured Supabase table, and `most_recent_edit` must be comparable with the stored timestamp.