bridgetown_directus 0.2.0 → 0.4.1

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: dd3625cab0b39f5374ad7a3647e5b6e369bed783f05412cb38e15e8f10208908
4
- data.tar.gz: a99f097216b2c9239188563a288b0df5752709782c29bfe15673edd73fd6c80d
3
+ metadata.gz: 2212f8e06630de61f4abdc8f50fb65339d3928e9a39e6658fd16f5af37986fdd
4
+ data.tar.gz: 2e02c25a8dcf030d9497caab17a1970c1170151ef777c26538ef2d0f47fb06c2
5
5
  SHA512:
6
- metadata.gz: d70b609eab2f9c4015c4e8898324fe046f8a62673c51d11057ec166372cceb7dfc3cba9793b1a37e3dd175f150d1d2dbc2c7c371427e740b61303630228793f0
7
- data.tar.gz: 1dafe4be86342c4196ca93700035ad75f99b0a6a7d0ab78f0592f8ec0a76e51a223b7b63a7f46bf1b0568593b8322b0428f95e5329f38788c55d0a9b07907a9a
6
+ metadata.gz: 8843466b19c819f8f8cbc3e36188524f4f46774f95ee79b9cd4b2042fd39a38a640accc54addae8666e0249384619ec23ea3c2379daf5d91c0085f3e8019009a
7
+ data.tar.gz: bc119472aa06bfebf3a09357f622224248f4405b5f5bb8d895c215e71b4ddff7d7be2064eac98940dc3c8ebf6857d617ec6529004b7f4cd5da8430930917c73c
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: ruby/setup-ruby@v1
19
+ with:
20
+ ruby-version: "3.4"
21
+ bundler-cache: true
22
+ - name: Run tests
23
+ run: bundle exec rake test
@@ -0,0 +1,21 @@
1
+ name: Release Please
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: write
11
+ pull-requests: write
12
+
13
+ jobs:
14
+ release-please:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: googleapis/release-please-action@v4
18
+ with:
19
+ token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
20
+ config-file: .release-please-config.json
21
+ manifest-file: .release-please-manifest.json
@@ -0,0 +1,28 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "bridgetown_directus/v*"
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+ id-token: write
12
+
13
+ jobs:
14
+ publish:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: ruby/setup-ruby@v1
19
+ with:
20
+ ruby-version: "3.4"
21
+ bundler-cache: true
22
+ - name: Run tests
23
+ run: bundle exec rake test
24
+ - name: Build gem
25
+ run: bundle exec rake build
26
+ - uses: rubygems/configure-rubygems-credentials@v1.0.0
27
+ - name: Push gem
28
+ run: gem push pkg/*.gem
data/.gitignore CHANGED
@@ -38,3 +38,6 @@ test/dest
38
38
  .bridgetown-webpack
39
39
 
40
40
  .env
41
+
42
+ # MacOS
43
+ .DS_Store
@@ -0,0 +1,10 @@
1
+ {
2
+ "packages": {
3
+ ".": {
4
+ "release-type": "ruby",
5
+ "package-name": "bridgetown_directus",
6
+ "changelog-path": "CHANGELOG.md",
7
+ "include-v-in-tag": true
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.1"
3
+ }
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
+ ## [0.4.1](https://github.com/Munkun-Estudio/bridgetown_directus/compare/bridgetown_directus/v0.4.0...bridgetown_directus/v0.4.1) (2026-02-16)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * match release-please tag format in release workflow ([70bd081](https://github.com/Munkun-Estudio/bridgetown_directus/commit/70bd08125477acb33f10e06de5fd41ba889d472a))
14
+
15
+ ## [0.4.0](https://github.com/Munkun-Estudio/bridgetown_directus/compare/bridgetown_directus/v0.3.0...bridgetown_directus/v0.4.0) (2026-02-16)
16
+
17
+
18
+ ### Features
19
+
20
+ * add data collections, singleton support, flatten_m2m, and BridgetownDirectus.configure API ([31e99ac](https://github.com/Munkun-Estudio/bridgetown_directus/commit/31e99ac31463b944d20de1e81931219d2c0de59a))
21
+ * make SSL verify configurable via Configuration#ssl_verify ([6cb9033](https://github.com/Munkun-Estudio/bridgetown_directus/commit/6cb90331b0b2e99bd26ecde862c26d6969b0fbf6))
22
+
23
+ ## [0.3.0](https://github.com/Munkun-Estudio/bridgetown_directus/compare/bridgetown_directus-v0.2.0...bridgetown_directus/v0.3.0) (2026-01-27)
24
+
25
+
26
+ ### Features
27
+
28
+ * improve builder mapping and release automation ([b87c719](https://github.com/Munkun-Estudio/bridgetown_directus/commit/b87c719cfec175a86bc1f9388c308c496035d2cf))
29
+
8
30
  ## [Unreleased]
9
31
 
10
32
  - ...
data/README.md CHANGED
@@ -2,117 +2,223 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/bridgetown_directus.svg)](https://badge.fury.io/rb/bridgetown_directus)
4
4
 
5
- This Bridgetown plugin integrates with [Directus](https://directus.io/), a flexible headless CMS. The plugin allows Bridgetown to pull content from a Directus API during the build process and generate static content in your site. It supports both single-language and multilingual content through Directus translations.
5
+ A [Bridgetown](https://www.bridgetownrb.com/) plugin that syncs content from [Directus](https://directus.io/) at build time. It fetches collections from the Directus API and either generates static Markdown files or injects data directly into `site.data`.
6
6
 
7
7
  ## Features
8
8
 
9
- - Fetch content from **multiple Directus collections** during the build process
10
- - Support for **flexible field mapping** and custom converters
11
- - Support for **multilingual content** via Directus translations
12
- - **Experimental**: Advanced **filtering, sorting, and pagination** options
13
- - Simple configuration for any Bridgetown collection (posts, pages, or custom types)
9
+ - **Output collections** fetch Directus items and generate Markdown files (posts, pages, custom types)
10
+ - **Data collections** fetch Directus items and inject into `site.data` (no file generation)
11
+ - **Singletons** single-object collections (e.g. site settings)
12
+ - **M2M junction flattening** unwrap Directus many-to-many junction objects automatically
13
+ - **Flexible field mapping** with custom converters
14
+ - **Multilingual content** via Directus translations
15
+ - **Configurable SSL verification** for environments with strict OpenSSL
16
+ - **Graceful skip** — build succeeds even without Directus credentials configured
14
17
 
15
18
  ## Installation
16
19
 
17
- Before installing the plugin, make sure you have an [Auth Token](https://docs.directus.io/reference/authentication.html#access-tokens) in your Directus instance.
20
+ ### Recommended (Bridgetown Automation)
18
21
 
19
- ### Recommended Installation (Bridgetown Automation)
22
+ ```bash
23
+ bin/bridgetown apply https://github.com/munkun-estudio/bridgetown_directus
24
+ ```
20
25
 
21
- 1. Run the plugin's automation setup:
26
+ ### Manual
22
27
 
23
- ```bash
24
- bin/bridgetown apply https://github.com/munkun-estudio/bridgetown_directus
25
- ```
28
+ ```ruby
29
+ bundle add "bridgetown_directus"
30
+ ```
26
31
 
27
- This will:
28
- - Prompt for your Directus API URL, token, Directus collection name, and Bridgetown collection name
29
- - Generate a minimal `config/initializers/bridgetown_directus.rb`
30
- - All further customization is done in Ruby, not YAML
32
+ ```bash
33
+ bundle install
34
+ ```
31
35
 
32
- ### Manual Installation
36
+ ## Configuration
33
37
 
34
- 1. Add the gem to your Gemfile:
38
+ ### Basic Setup
35
39
 
36
- ```ruby
37
- bundle add "bridgetown_directus"
38
- ```
40
+ ```ruby
41
+ # config/initializers.rb
42
+ init :bridgetown_directus
39
43
 
40
- 2. Run `bundle install` to install the gem.
41
- 3. Create `config/initializers/bridgetown_directus.rb` (see below for configuration).
44
+ BridgetownDirectus.configure do |directus|
45
+ directus.api_url = ENV["DIRECTUS_API_URL"]
46
+ directus.token = ENV["DIRECTUS_API_TOKEN"]
42
47
 
43
- ## Configuration
48
+ directus.register_collection(:posts) do |c|
49
+ c.endpoint = "blog_posts"
50
+ c.layout = "post"
51
+ end
52
+ end
53
+ ```
54
+
55
+ The plugin reads `DIRECTUS_API_URL` and `DIRECTUS_API_TOKEN` from the environment by default. If neither is set, the build skips Directus sync gracefully.
44
56
 
45
- ### Minimal Example
57
+ ### Data Collections
58
+
59
+ Data collections populate `site.data` without generating files. Useful for navigation, settings, or any shared data.
46
60
 
47
61
  ```ruby
48
- # config/initializers/bridgetown_directus.rb
49
- init :bridgetown_directus do |directus|
50
- directus.api_url = ENV["DIRECTUS_API_URL"] || "https://your-directus-instance.com"
51
- directus.token = ENV["DIRECTUS_API_TOKEN"] || "your-token"
62
+ directus.register_collection(:navigation) do |c|
63
+ c.endpoint = "navigation_items"
64
+ c.resource_type = :data
65
+ c.default_query = {
66
+ sort: "sort",
67
+ filter: { status: { _eq: "published" } }.to_json
68
+ }
69
+ end
70
+ ```
52
71
 
53
- directus.register_collection(:posts) do |c|
54
- c.endpoint = "posts"
55
- c.layout = "post" # Use the singular layout for individual pages
56
- # Minimal mapping (optional):
57
- c.field :id, "id"
58
- c.field :title, "title"
59
- # To enable translations, uncomment and edit:
60
- # c.enable_translations([:title, :content])
61
- end
72
+ Access in templates via `site.data.navigation`.
73
+
74
+ ### Singletons
75
+
76
+ For collections that contain a single record (e.g. site settings):
77
+
78
+ ```ruby
79
+ directus.register_collection(:site_settings) do |c|
80
+ c.endpoint = "site_settings"
81
+ c.resource_type = :data
82
+ c.singleton = true
62
83
  end
63
84
  ```
64
85
 
65
- For custom collections, create a layout file at `src/_layouts/[singular].erb` (e.g., `staff_member.erb`) to control the page rendering.
86
+ Returns a single hash instead of an array. Access via `site.data.site_settings`.
66
87
 
67
- **By default, all Directus fields will be written to the front matter of generated Markdown files.**
68
- You only need to declare fields with `c.field` if you want to:
69
- - Rename a field in the output
70
- - Transform/convert a field value (e.g., format a date, generate a slug, etc.)
71
- - Set a default value if a field is missing
88
+ ### Output Collections
72
89
 
73
- #### Example: Customizing a Field
90
+ Generate Markdown files for posts, pages, or custom collection types:
91
+
92
+ ```ruby
93
+ directus.register_collection(:events) do |c|
94
+ c.endpoint = "events"
95
+ c.resource_type = :custom_collection
96
+ c.layout = "event"
97
+ c.default_query = {
98
+ filter: { status: { _eq: "published" } }.to_json
99
+ }
100
+ end
101
+ ```
102
+
103
+ Generated files include `directus_generated: true` in front matter. Only these files are cleaned up on rebuild — user-authored files are never deleted.
104
+
105
+ ### M2M Junction Flattening
106
+
107
+ Directus returns many-to-many relationships wrapped in junction objects:
108
+
109
+ ```json
110
+ [{ "raus_stats_id": { "id": 1, "value": "8+" } }]
111
+ ```
112
+
113
+ Use `flatten_m2m` to unwrap them:
114
+
115
+ ```ruby
116
+ directus.register_collection(:pages) do |c|
117
+ c.endpoint = "pages"
118
+ c.resource_type = :data
119
+ c.default_query = {
120
+ fields: "id,title,sections.stats.raus_stats_id.*"
121
+ }
122
+ c.flatten_m2m "sections.stats", key: "raus_stats_id"
123
+ end
124
+ ```
125
+
126
+ After flattening:
127
+
128
+ ```json
129
+ [{ "id": 1, "value": "8+" }]
130
+ ```
131
+
132
+ ### Field Mapping
133
+
134
+ All Directus fields are included in the output by default. Use `c.field` only when you need to rename or transform a value:
74
135
 
75
136
  ```ruby
76
137
  c.field :slug, "slug" do |value|
77
- value || "staff_member-#{SecureRandom.hex(4)}"
138
+ value || "fallback-#{SecureRandom.hex(4)}"
78
139
  end
79
140
  ```
80
141
 
81
142
  ### Translations
82
143
 
83
- To enable translations for specific fields, add this inside your collection block:
84
-
85
144
  ```ruby
86
145
  c.enable_translations([:title, :content])
87
146
  ```
88
147
 
89
- - You can list any field that exists in your Directus collection, even if it's not declared above with `c.field`.
90
- - Only declare a field with `c.field` if you want to rename, transform, or set a default for it.
148
+ ### SSL Verification
149
+
150
+ If your local OpenSSL (3.6+) fails with CRL verification errors, you can disable SSL verification:
151
+
152
+ ```ruby
153
+ directus.ssl_verify = false
154
+ ```
155
+
156
+ This is a client-side issue with recent OpenSSL versions that enforce CRL checking but cannot auto-download CRLs during the TLS handshake. It typically only affects local development — CI/CD environments use standard OpenSSL builds without this issue.
91
157
 
92
- ### File Generation & Cleanup
158
+ Default: `true`.
93
159
 
94
- - **Generated files**: The plugin writes Markdown files to `src/_[bridgetown_collection]/` (e.g., `src/_staff_members/`).
95
- - **Safety**: Only files with the `directus_generated: true` flag in their front matter are deleted during cleanup. User-authored files are never removed.
160
+ ### Debug Logging
96
161
 
97
- ### Advanced Configuration
162
+ ```bash
163
+ BRIDGETOWN_DIRECTUS_LOG=1 bin/bt build
164
+ ```
98
165
 
99
- See the plugin source and inline documentation for advanced features such as:
100
- - Multiple collections
101
- - Custom layouts per collection
102
- - Filtering, sorting, and pagination via `c.default_query` (**experimental**; not fully tested in production—see notes below)
103
- - Selective field output
166
+ ### Environment Variables
104
167
 
105
- **Note:** Filtering, sorting, and pagination via `c.default_query` is experimental and not yet fully tested in real Bridgetown projects. Please report issues or contribute test cases if you use this feature!
168
+ | Variable | Description |
169
+ | -------- | ----------- |
170
+ | `DIRECTUS_API_URL` | Directus instance URL |
171
+ | `DIRECTUS_API_TOKEN` | Static access token |
172
+ | `DIRECTUS_TOKEN` | Legacy fallback for token |
173
+ | `BRIDGETOWN_DIRECTUS_LOG` | Set to `1` for verbose logging |
174
+
175
+ ## Full Example
176
+
177
+ ```ruby
178
+ # config/initializers.rb
179
+ init :bridgetown_directus
180
+
181
+ BridgetownDirectus.configure do |directus|
182
+ directus.api_url = ENV["DIRECTUS_API_URL"]
183
+ directus.token = ENV["DIRECTUS_API_TOKEN"]
184
+ directus.ssl_verify = false
185
+
186
+ # Data-only: populates site.data
187
+ directus.register_collection(:site_settings) do |c|
188
+ c.endpoint = "site_settings"
189
+ c.resource_type = :data
190
+ c.singleton = true
191
+ end
106
192
 
107
- ### Migrating from 0.1.x
193
+ directus.register_collection(:navigation) do |c|
194
+ c.endpoint = "navigation_items"
195
+ c.resource_type = :data
196
+ c.default_query = { sort: "sort" }
197
+ end
108
198
 
109
- - **YAML config is no longer used.** All configuration is now in Ruby in `config/initializers/bridgetown_directus.rb`.
110
- - Field mapping, transformation, and translations are handled in the initializer.
111
- - All Directus fields are output by default; use `c.field` for customization.
112
- - **Upgrading?** The `resource_type` option is no longer required. Use the Bridgetown collection name and layout instead. See the [CHANGELOG](CHANGELOG.md) for details.
199
+ # Output: generates Markdown files
200
+ directus.register_collection(:events) do |c|
201
+ c.endpoint = "events"
202
+ c.resource_type = :custom_collection
203
+ c.layout = "event"
204
+ c.default_query = {
205
+ filter: { status: { _eq: "published" } }.to_json
206
+ }
207
+ end
208
+ end
209
+ ```
113
210
 
114
- ---
211
+ ## Migrating from 0.2.x
115
212
 
116
- For more details and advanced usage, see the [plugin README](https://github.com/Munkun-Estudio/bridgetown_directus).
213
+ - Configuration now uses `BridgetownDirectus.configure` block after `init :bridgetown_directus`
214
+ - New `resource_type: :data` for collections that inject into `site.data`
215
+ - New `singleton: true` for single-record collections
216
+ - New `flatten_m2m` for M2M junction unwrapping
217
+ - Build no longer fails when Directus credentials are missing — it skips gracefully
218
+ - SSL verification is configurable via `ssl_verify`
117
219
 
118
220
  See [CHANGELOG.md](CHANGELOG.md) for upgrade notes and detailed changes.
221
+
222
+ ## License
223
+
224
+ See [LICENSE](LICENSE) for details.
data/RELEASING.md ADDED
@@ -0,0 +1,109 @@
1
+ # Releasing `bridgetown_directus`
2
+
3
+ This document explains the release workflow for this gem and how to recover if automation fails.
4
+
5
+ ## Overview
6
+
7
+ There are three parts:
8
+
9
+ 1. **release-please** creates a release PR and a `vX.Y.Z` tag.
10
+ 2. **Release** workflow publishes the gem to RubyGems.
11
+ 3. **CI** runs tests on PRs and pushes.
12
+
13
+ ## Normal automated release flow (recommended)
14
+
15
+ 1. **Make changes on a branch** and merge to `main` with a Conventional Commit title:
16
+ - `fix:` → patch (e.g., `0.3.1`)
17
+ - `feat:` → minor (e.g., `0.4.0`)
18
+ - `feat!:` or `BREAKING CHANGE:` → major (e.g., `1.0.0`)
19
+ 2. `release-please` runs on push to `main` and **opens a release PR**.
20
+ 3. Merge the release PR:
21
+ - `release-please` **creates a tag** `vX.Y.Z` and a GitHub Release.
22
+ 4. The **Release workflow** runs on the tag and **publishes to RubyGems** via Trusted Publishing.
23
+
24
+ ### Requirements for the automated flow
25
+
26
+ - A **PAT** stored as the repo secret `RELEASE_PLEASE_TOKEN` so the tag created by release-please triggers other workflows.
27
+ - RubyGems **Trusted Publisher** configured for this repo/workflow (see below).
28
+
29
+ ## Workflows (GitHub Actions)
30
+
31
+ ### CI
32
+
33
+ File: `.github/workflows/ci.yml`
34
+ Runs tests on PRs and pushes.
35
+
36
+ ### Release Please
37
+
38
+ File: `.github/workflows/release-please.yml`
39
+ Creates release PRs and tags.
40
+
41
+ Important: it uses a PAT:
42
+
43
+ ```yaml
44
+ token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
45
+ ```
46
+
47
+ ### Release
48
+
49
+ File: `.github/workflows/release.yml`
50
+ Runs tests and publishes using OIDC (Trusted Publishing).
51
+
52
+ ## Secrets you need
53
+
54
+ 1. **RELEASE_PLEASE_TOKEN** (GitHub PAT, classic token with `repo` scope)
55
+ - Used by release-please to create tags that trigger the Release workflow.
56
+
57
+ > NOTE: You no longer need `RUBYGEMS_API_KEY` once Trusted Publishing is configured.
58
+
59
+ ## RubyGems Trusted Publishing setup
60
+
61
+ 1. Go to RubyGems → your gem → **Trusted Publishers** → **Create**.
62
+ 2. Fill in:
63
+ - Owner: `Munkun-Estudio`
64
+ - Repo: `bridgetown_directus`
65
+ - Workflow file: `release.yml`
66
+ - Environment: leave blank (unless you use one in GitHub)
67
+ 3. Save.
68
+
69
+ After this, GitHub can publish without API keys or OTP.
70
+
71
+ ## Local release (manual fallback)
72
+
73
+ Use this if the Release workflow fails or you need to publish immediately:
74
+
75
+ ```bash
76
+ cd /Users/pablo/projects/bridgetown_directus
77
+ bundle install
78
+ bundle exec rake build
79
+ ls pkg
80
+ gem push pkg/bridgetown_directus-X.Y.Z.gem
81
+ ```
82
+
83
+ If RubyGems MFA is enabled, you will be prompted for an OTP.
84
+
85
+ ## Troubleshooting
86
+
87
+ ### Release PR merged but gem not published
88
+
89
+ Likely cause: tag created by `release-please` didn’t trigger the Release workflow.
90
+ Fix: ensure `RELEASE_PLEASE_TOKEN` is set and the workflow uses it.
91
+
92
+ ### Release workflow failed with MFA/OTP
93
+
94
+ You are not using Trusted Publishing. Configure it as above, then re-run the Release workflow.
95
+
96
+ ### Tag exists but no Release workflow run
97
+
98
+ 1. Verify the tag is `vX.Y.Z`.
99
+ 2. Go to **Actions → Release → Run workflow** (manual run).
100
+ 3. If it still fails, check the workflow logs.
101
+
102
+ ## Version file
103
+
104
+ `lib/bridgetown_directus/version.rb` is updated by release-please.
105
+ Do **not** bump it manually if you use release-please.
106
+
107
+ ## Changelog
108
+
109
+ `CHANGELOG.md` is updated by release-please. Keep entries under `Unreleased` if editing manually.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
4
+ require "fileutils"
3
5
  require "yaml"
4
6
 
5
7
  module BridgetownDirectus
@@ -7,17 +9,16 @@ module BridgetownDirectus
7
9
  def build
8
10
  config = site.config.bridgetown_directus
9
11
  return if site.ssr?
12
+ return unless config&.api_url && config&.token
13
+
14
+ client = Client.new(api_url: config.api_url, token: config.token, ssl_verify: config.ssl_verify)
10
15
 
11
16
  config.collections.each_value do |collection_config|
12
- next unless [:posts, :pages, :custom_collection].include?(collection_config.resource_type)
13
-
14
- process_collection(
15
- client: Client.new(
16
- api_url: config.api_url,
17
- token: config.token
18
- ),
19
- collection_config: collection_config
20
- )
17
+ if collection_config.data?
18
+ process_data_collection(client: client, collection_config: collection_config)
19
+ elsif [:posts, :pages, :custom_collection].include?(collection_config.resource_type)
20
+ process_collection(client: client, collection_config: collection_config)
21
+ end
21
22
  end
22
23
  end
23
24
 
@@ -35,23 +36,84 @@ module BridgetownDirectus
35
36
  end
36
37
  end
37
38
 
38
- # Write a Directus item as a Markdown file in the correct Bridgetown collection directory
39
- def write_directus_file(item, collection_dir, layout = nil, api_url = nil)
40
- require "fileutils"
41
- FileUtils.mkdir_p(collection_dir)
42
- slug = item["slug"] || item["id"].to_s
43
- filename = build_filename(collection_dir, slug)
44
- item = transform_item_fields(item, api_url, layout)
45
- item["directus_generated"] = true # Add flag to front matter
46
- content = item.delete("body") || ""
47
- front_matter = generate_front_matter(item)
48
- write_markdown_file(filename, front_matter, content)
39
+ def build_directus_payload(item, collection_dir, collection_config, api_url = nil)
40
+ mapped_item = apply_data_mapping(item, collection_config)
41
+ slug = normalize_slug(mapped_item)
42
+ mapped_item["slug"] = slug
43
+ filename = build_filename(collection_dir, collection_config, mapped_item, slug)
44
+ mapped_item = transform_item_fields(mapped_item, api_url, collection_config.layout)
45
+ mapped_item["directus_generated"] = true # Add flag to front matter
46
+ content = mapped_item.delete("body") || ""
47
+ front_matter = generate_front_matter(mapped_item)
48
+ payload = render_markdown(front_matter, content)
49
+ [filename, payload]
49
50
  end
50
51
 
51
- def build_filename(collection_dir, slug)
52
+ def build_filename(collection_dir, collection_config, item, slug)
53
+ if collection_config.resource_type == :posts || collection_config.name.to_s == "posts"
54
+ post_date = extract_post_date(item)
55
+ if post_date
56
+ return File.join(collection_dir, "#{post_date.strftime("%Y-%m-%d")}-#{slug}.md")
57
+ end
58
+ end
59
+
52
60
  File.join(collection_dir, "#{slug}.md")
53
61
  end
54
62
 
63
+ def normalize_slug(item)
64
+ slug = item["slug"] || item[:slug]
65
+ slug = slug.to_s.strip
66
+ return slug unless slug.empty?
67
+
68
+ title = item["title"] || item[:title]
69
+ if title && defined?(Bridgetown::Utils) && Bridgetown::Utils.respond_to?(:slugify)
70
+ slug = Bridgetown::Utils.slugify(title.to_s)
71
+ else
72
+ slug = title.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
73
+ end
74
+
75
+ slug = slug.strip
76
+ return slug unless slug.empty?
77
+
78
+ id = item["id"] || item[:id]
79
+ id.to_s
80
+ end
81
+
82
+ def apply_data_mapping(item, collection_config)
83
+ mapped_item = item.dup
84
+ return mapped_item unless collection_config.fields.any? || collection_config.translations_enabled
85
+
86
+ mapped_fields = if collection_config.translations_enabled
87
+ DataMapper.map_translations(collection_config, item, resolve_locale)
88
+ else
89
+ DataMapper.map(collection_config, item)
90
+ end
91
+
92
+ mapped_item.merge!(mapped_fields)
93
+ mapped_item
94
+ end
95
+
96
+ def resolve_locale
97
+ return site.locale if site.respond_to?(:locale) && site.locale
98
+
99
+ config_locale = site.config["locale"] || site.config[:locale]
100
+ return config_locale.to_sym if config_locale
101
+
102
+ :en
103
+ end
104
+
105
+ def extract_post_date(item)
106
+ raw = item["date"] || item[:date] || item["published_at"] || item[:published_at] || item["date_created"]
107
+ item["date"] ||= item["published_at"] if item["published_at"] && !item["date"]
108
+ return nil unless raw
109
+
110
+ return raw.to_date if raw.respond_to?(:to_date)
111
+
112
+ Date.parse(raw.to_s)
113
+ rescue ArgumentError
114
+ nil
115
+ end
116
+
55
117
  def transform_item_fields(item, api_url, layout)
56
118
  item = item.dup
57
119
  if item["image"] && api_url && !item["image"].to_s.start_with?("http://", "https://")
@@ -66,19 +128,69 @@ module BridgetownDirectus
66
128
  yaml.sub(%r{^---\s*\n}, "") # Remove leading --- if present
67
129
  end
68
130
 
69
- def write_markdown_file(filename, front_matter, content)
70
- File.write(filename, "---\n#{front_matter}---\n\n#{content}")
131
+ def render_markdown(front_matter, content)
132
+ "---\n#{front_matter}---\n\n#{content}"
133
+ end
134
+
135
+ def write_markdown_file(filename, payload)
136
+ FileUtils.mkdir_p(File.dirname(filename))
137
+ File.write(filename, payload)
138
+ end
139
+
140
+ def file_unchanged?(filename, payload)
141
+ return false unless File.exist?(filename)
142
+
143
+ File.read(filename) == payload
144
+ rescue StandardError
145
+ false
71
146
  end
72
147
 
73
148
  # Remove only plugin-generated Markdown files in the target directory before writing new ones
74
- def clean_collection_directory(collection_dir)
75
- require "yaml"
149
+ def clean_collection_directory(collection_dir, keep_files: [])
150
+ keep_set = keep_files.map { |file| File.expand_path(file) }.to_h { |file| [file, true] }
151
+
152
+ deleted = 0
76
153
  Dir.glob(File.join(collection_dir, "*.md")).each do |file|
154
+ next if keep_set[File.expand_path(file)]
155
+
77
156
  fm = File.read(file)[%r{\A---.*?---}m]
78
- File.delete(file) if fm && YAML.safe_load(fm)["directus_generated"]
157
+ if fm && YAML.safe_load(fm, permitted_classes: [Date, Time, DateTime])["directus_generated"]
158
+ File.delete(file)
159
+ deleted += 1
160
+ end
79
161
  rescue StandardError => e
80
162
  warn "[BridgetownDirectus] Could not check/delete #{file}: #{e.message}"
81
163
  end
164
+
165
+ deleted
166
+ end
167
+
168
+ # Fetch a data-only collection and inject into site.data (no file generation)
169
+ def process_data_collection(client:, collection_config:)
170
+ endpoint = collection_config.endpoint || collection_config.name.to_s
171
+ begin
172
+ response = client.fetch_collection(endpoint, collection_config.default_query)
173
+ rescue StandardError => e
174
+ warn "Error fetching data collection '#{endpoint}': #{e.message}"
175
+ return
176
+ end
177
+
178
+ process_data_collection_with_data(response, collection_config)
179
+ end
180
+
181
+ # Process already-fetched data for a data collection (also used in tests)
182
+ def process_data_collection_with_data(response, collection_config)
183
+ data = sanitize_keys(response)
184
+
185
+ if collection_config.singleton
186
+ data = data.is_a?(Array) ? data.first : data
187
+ end
188
+
189
+ # Apply M2M flattening if configured
190
+ apply_m2m_flattenings!(data, collection_config)
191
+
192
+ site.data[collection_config.name.to_s] = data
193
+ log_directus("Loaded data collection: #{collection_label(collection_config)}#{collection_config.singleton ? ' (singleton)' : " (#{Array(data).size} items)"}")
82
194
  end
83
195
 
84
196
  def process_collection(client:, collection_config:)
@@ -90,14 +202,79 @@ module BridgetownDirectus
90
202
  return
91
203
  end
92
204
  collection_dir = collection_directory(collection_config.name)
93
- clean_collection_directory(collection_dir)
205
+ FileUtils.mkdir_p(collection_dir)
94
206
  api_url = site.config.bridgetown_directus.api_url
95
207
  sanitized_response = sanitize_keys(response)
96
- sanitized_response.each do |item|
97
- write_directus_file(item, collection_dir, collection_config.layout, api_url)
208
+ payloads = sanitized_response.to_h do |item|
209
+ build_directus_payload(item, collection_dir, collection_config, api_url)
210
+ end
211
+ log_directus("Generating #{collection_label(collection_config)} (#{payloads.size} items)")
212
+ deleted = clean_collection_directory(collection_dir, keep_files: payloads.keys)
213
+ written = 0
214
+ skipped = 0
215
+ payloads.each do |filename, payload|
216
+ if file_unchanged?(filename, payload)
217
+ skipped += 1
218
+ next
219
+ end
220
+
221
+ write_markdown_file(filename, payload)
222
+ written += 1
223
+ end
224
+ log_directus("Updated #{collection_label(collection_config)}: wrote #{written}, skipped #{skipped}, deleted #{deleted}")
225
+ end
226
+
227
+ # Apply M2M junction flattening to fetched data.
228
+ # Walks the configured dot-paths and unwraps junction objects.
229
+ def apply_m2m_flattenings!(data, collection_config)
230
+ return if collection_config.m2m_flattenings.empty?
231
+
232
+ items = data.is_a?(Array) ? data : [data].compact
233
+ collection_config.m2m_flattenings.each do |flattening|
234
+ path_parts = flattening[:path].split(".")
235
+ junction_key = flattening[:key]
236
+ items.each { |item| flatten_at_path!(item, path_parts, junction_key) }
237
+ end
238
+ end
239
+
240
+ # Recursively walk a dot-path and flatten the M2M array at the leaf.
241
+ def flatten_at_path!(obj, path_parts, junction_key)
242
+ return unless obj.is_a?(Hash)
243
+
244
+ key = path_parts.first
245
+ remaining = path_parts[1..]
246
+
247
+ if remaining.empty?
248
+ # We're at the leaf — flatten the junction array
249
+ return unless obj[key].is_a?(Array)
250
+
251
+ obj[key] = obj[key].filter_map { |junction| junction[junction_key] if junction.is_a?(Hash) }
252
+ else
253
+ # Intermediate path — recurse into nested object(s)
254
+ target = obj[key]
255
+ if target.is_a?(Array)
256
+ target.each { |nested| flatten_at_path!(nested, remaining, junction_key) }
257
+ elsif target.is_a?(Hash)
258
+ flatten_at_path!(target, remaining, junction_key)
259
+ end
98
260
  end
99
261
  end
100
262
 
263
+ def log_directus(message)
264
+ return unless directus_logging_enabled?
265
+
266
+ Utils.log_directus(message)
267
+ end
268
+
269
+ def directus_logging_enabled?
270
+ flag = ENV["BRIDGETOWN_DIRECTUS_LOG"]
271
+ flag && !flag.to_s.strip.empty? && flag.to_s != "0"
272
+ end
273
+
274
+ def collection_label(collection_config)
275
+ collection_config.name.to_s.tr("_", " ").split.map(&:capitalize).join(" ")
276
+ end
277
+
101
278
  # Recursively sanitize keys to avoid illegal instance variable names (Ruby 3.4+)
102
279
  def sanitize_keys(obj)
103
280
  case obj
@@ -8,9 +8,10 @@ module BridgetownDirectus
8
8
  class Client
9
9
  attr_reader :api_url, :token
10
10
 
11
- def initialize(api_url:, token:)
11
+ def initialize(api_url:, token:, ssl_verify: true)
12
12
  @api_url = api_url
13
13
  @token = token
14
+ @ssl_verify = ssl_verify
14
15
  return unless @token.nil? || @api_url.nil?
15
16
 
16
17
  raise StandardError, "Invalid Directus configuration: missing API token or URL"
@@ -45,7 +46,7 @@ module BridgetownDirectus
45
46
  private
46
47
 
47
48
  def connection
48
- @connection ||= Faraday.new(url: @api_url) do |faraday|
49
+ @connection ||= Faraday.new(url: @api_url, ssl: { verify: @ssl_verify }) do |faraday|
49
50
  faraday.headers["Authorization"] = "Bearer #{@token}"
50
51
  faraday.headers["Content-Type"] = "application/json"
51
52
  faraday.adapter Faraday.default_adapter
@@ -43,11 +43,33 @@ module BridgetownDirectus
43
43
  @translations_enabled = false
44
44
  @translatable_fields = []
45
45
  @endpoint = nil
46
+ @singleton = false
47
+ @m2m_flattenings = []
46
48
  end
47
49
 
48
50
  # Set up accessors for collection configuration properties
49
51
  attr_accessor :endpoint, :fields, :default_query, :resource_type, :layout,
50
- :translations_enabled, :translatable_fields
52
+ :translations_enabled, :translatable_fields, :singleton
53
+ attr_reader :m2m_flattenings
54
+
55
+ # Register a many-to-many junction to flatten after fetching.
56
+ # Directus returns M2M data wrapped in junction objects like:
57
+ # [{"raus_stats_id": {"id": 1, "value": "8+"}}]
58
+ # This unwraps them to:
59
+ # [{"id": 1, "value": "8+"}]
60
+ #
61
+ # @param path [String] Dot-separated path to the M2M field (e.g. "sections.stats")
62
+ # @param key [String] The junction key containing the actual related item
63
+ # @return [void]
64
+ def flatten_m2m(path, key:)
65
+ @m2m_flattenings << { path: path, key: key }
66
+ end
67
+
68
+ # Check if this collection is a data-only collection
69
+ # @return [Boolean]
70
+ def data?
71
+ @resource_type == :data
72
+ end
51
73
 
52
74
  # Define a field mapping with optional converter
53
75
  # @param bridgetown_field [Symbol] The field name in Bridgetown
@@ -113,7 +113,15 @@ module BridgetownDirectus
113
113
  # Get the Directus field name for this Bridgetown field
114
114
  field_config = collection_config.fields[field]
115
115
 
116
- directus_field = field_config.is_a?(Hash) ? field_config[:directus_field] : field_config.to_s
116
+ directus_field = if field_config.nil?
117
+ field.to_s
118
+ elsif field_config.is_a?(Hash)
119
+ field_config[:directus_field]
120
+ else
121
+ field_config.to_s
122
+ end
123
+
124
+ directus_field = field.to_s if directus_field.to_s.empty?
117
125
 
118
126
  # Check if the translation has this field
119
127
  next unless translation[directus_field]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BridgetownDirectus
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -8,25 +8,53 @@ require_relative "bridgetown_directus/configuration"
8
8
  require_relative "bridgetown_directus/builder"
9
9
 
10
10
  module BridgetownDirectus
11
- # Bridgetown initializer for the plugin
12
- Bridgetown.initializer :bridgetown_directus do |config|
13
- # Only assign config.bridgetown_directus if not already set
14
- config.bridgetown_directus ||= Configuration.new
11
+ # Bridgetown initializer for the plugin.
12
+ #
13
+ # Usage in config/initializers.rb:
14
+ #
15
+ # init :bridgetown_directus do
16
+ # api_url ENV["DIRECTUS_API_URL"]
17
+ # token ENV["DIRECTUS_API_TOKEN"]
18
+ # end
19
+ #
20
+ # Then configure collections separately:
21
+ #
22
+ # BridgetownDirectus.configure do |directus|
23
+ # directus.register_collection(:posts) { |c| ... }
24
+ # end
25
+ #
26
+ Bridgetown.initializer :bridgetown_directus do |config, **kwargs|
27
+ bd_config = Configuration.instance
15
28
 
16
- # Set up configuration directly (leave to user initializer if possible)
17
- config.bridgetown_directus.api_url ||= ENV["DIRECTUS_API_URL"] || "[https://studio.munkun.com](https://studio.munkun.com)"
18
- config.bridgetown_directus.token ||= ENV["DIRECTUS_TOKEN"] || "t1P6YstcUslmf-KJFbc6Kyg0bomMxkXY"
29
+ # Apply keyword args from init block (e.g. api_url, token)
30
+ bd_config.api_url ||= kwargs[:api_url]&.to_s || ENV["DIRECTUS_API_URL"]
31
+ bd_config.token ||= kwargs[:token]&.to_s || ENV["DIRECTUS_API_TOKEN"] || ENV["DIRECTUS_TOKEN"]
32
+
33
+ # Store on the Bridgetown config so the Builder can access it
34
+ config.bridgetown_directus = bd_config
19
35
 
20
36
  # Register the builder
21
37
  config.builder BridgetownDirectus::Builder
22
38
  end
23
39
 
40
+ # Global configuration singleton. Call BridgetownDirectus.configure to register collections.
24
41
  class Configuration
25
- attr_accessor :api_url, :token
42
+ attr_accessor :api_url, :token, :ssl_verify
26
43
  attr_reader :collections
27
44
 
28
45
  def initialize
29
46
  @collections = {}
47
+ @ssl_verify = true
48
+ end
49
+
50
+ # Returns the singleton Configuration instance
51
+ def self.instance
52
+ @instance ||= new
53
+ end
54
+
55
+ # Reset the singleton (useful in tests)
56
+ def self.reset!
57
+ @instance = nil
30
58
  end
31
59
 
32
60
  def register_collection(name, &block)
@@ -36,4 +64,15 @@ module BridgetownDirectus
36
64
  collection
37
65
  end
38
66
  end
67
+
68
+ # Configure the plugin. Call this after `init :bridgetown_directus`.
69
+ #
70
+ # BridgetownDirectus.configure do |directus|
71
+ # directus.api_url = ENV["DIRECTUS_API_URL"]
72
+ # directus.register_collection(:posts) { |c| ... }
73
+ # end
74
+ #
75
+ def self.configure
76
+ yield Configuration.instance if block_given?
77
+ end
39
78
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bridgetown_directus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Munkun
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-16 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bridgetown
@@ -160,12 +160,18 @@ executables: []
160
160
  extensions: []
161
161
  extra_rdoc_files: []
162
162
  files:
163
+ - ".github/workflows/ci.yml"
164
+ - ".github/workflows/release-please.yml"
165
+ - ".github/workflows/release.yml"
163
166
  - ".gitignore"
167
+ - ".release-please-config.json"
168
+ - ".release-please-manifest.json"
164
169
  - ".rubocop.yml"
165
170
  - CHANGELOG.md
166
171
  - Gemfile
167
172
  - LICENSE.txt
168
173
  - README.md
174
+ - RELEASING.md
169
175
  - Rakefile
170
176
  - bridgetown.automation.rb
171
177
  - bridgetown_directus.gemspec
@@ -201,7 +207,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
201
207
  - !ruby/object:Gem::Version
202
208
  version: '0'
203
209
  requirements: []
204
- rubygems_version: 3.6.2
210
+ rubygems_version: 3.6.9
205
211
  specification_version: 4
206
212
  summary: Use Directus as headless CMS for Bridgetown
207
213
  test_files: []