bridgetown_directus 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2311ee5e15d9cab7cd7cd04746afd03d17a85982e2cc43cdadcc25ffba3646cb
4
- data.tar.gz: 2cce7fa9fe95891b1559968d5e3d1c139752182966160a1fa6ac44933a6b36d3
3
+ metadata.gz: dd3625cab0b39f5374ad7a3647e5b6e369bed783f05412cb38e15e8f10208908
4
+ data.tar.gz: a99f097216b2c9239188563a288b0df5752709782c29bfe15673edd73fd6c80d
5
5
  SHA512:
6
- metadata.gz: 8aa565c2c68cb96330213ace4efcd99a31948aa7349594004de99018dc9195518ce6620e97f4e6f578d8def0354f4b2519e92f5128b1a57cb6179948f1f3900f
7
- data.tar.gz: 91b5f0df720865f92c388a27b2613dfeeed7c4b95190d2dd56699eaec6719f0c495be02f5ccd7504c748615ba7ca41851dd8d437aff089d84f4268712c48d8fd
6
+ metadata.gz: d70b609eab2f9c4015c4e8898324fe046f8a62673c51d11057ec166372cceb7dfc3cba9793b1a37e3dd175f150d1d2dbc2c7c371427e740b61303630228793f0
7
+ data.tar.gz: 1dafe4be86342c4196ca93700035ad75f99b0a6a7d0ab78f0592f8ec0a76e51a223b7b63a7f46bf1b0568593b8322b0428f95e5329f38788c55d0a9b07907a9a
data/CHANGELOG.md CHANGED
@@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  - ...
11
11
 
12
+ ## [0.2.0] - 2025-04-16
13
+
14
+ - BREAKING: Simplified configuration—`resource_type` is no longer required. Use the Bridgetown collection name and layout instead.
15
+ - Automation now prompts for both Directus and Bridgetown collection names and sets up the initializer accordingly.
16
+ - Generated files are now flagged with `directus_generated: true` in front matter for safe cleanup.
17
+ - Only plugin-generated files (with this flag) are deleted during cleanup; user-authored files are preserved.
18
+ - Layouts for custom collections are now singular (e.g., `staff_member.erb`).
19
+ - README and example configuration updated for new conventions.
20
+ - Test suite updated for custom collections and file safety logic.
21
+
12
22
  ## [0.1.0] - 2024-09-27
13
23
 
14
24
  - First version
data/README.md CHANGED
@@ -1,138 +1,118 @@
1
1
  # Bridgetown Directus Plugin
2
2
 
3
- This Bridgetown plugin integrates with the [Directus](https://directus.io/) which is among other things a [headless CMS](https://en.wikipedia.org/wiki/Headless_content_management_system). The plugin allows Bridgetown to pull content from a Directus API during the build process and generate static content in your site. It currently supports fetching published posts, with future plans for more flexibility and features.
3
+ [![Gem Version](https://badge.fury.io/rb/bridgetown_directus.svg)](https://badge.fury.io/rb/bridgetown_directus)
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.
4
6
 
5
7
  ## Features
6
8
 
7
- - Fetch **published posts** from Directus during the build process
8
- - Simple setup and configuration
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
14
 
10
15
  ## Installation
11
16
 
12
- Before installing the plugin make sure you have an [Auth Token](https://docs.directus.io/reference/authentication.html#access-tokens) in your Directus instance.
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.
13
18
 
14
19
  ### Recommended Installation (Bridgetown Automation)
15
20
 
16
21
  1. Run the plugin's automation setup:
22
+
17
23
  ```bash
18
24
  bin/bridgetown apply https://github.com/munkun-estudio/bridgetown_directus
19
25
  ```
20
- 2. The setup will guide you to provide the Directus API URL and Auth Token, and configure the plugin automatically.
26
+
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
21
31
 
22
32
  ### Manual Installation
23
33
 
24
- 1. Add the gem to your Gemfile:
25
- ```ruby
26
- bundle add "bridgetown_directus"
27
- ```
28
- 2. Run bundle install to install the gem.
29
- 3. Add the plugin configuration to your config/initializers.rb file:
30
- ```ruby
31
- init :bridgetown_directus do
32
- api_url "https://your-directus-instance.com"
33
- token ENV['DIRECTUS_AUTH_TOKEN'] || "your_token"
34
-
35
- # Required field mappings
36
- mappings do
37
- title "title"
38
- content "content"
39
- slug "slug"
40
- date "date"
41
- category "category"
42
- excerpt "excerpt"
43
- image "image"
44
- end
45
- end
46
- ```
34
+ 1. Add the gem to your Gemfile:
47
35
 
48
- ## Configuration
36
+ ```ruby
37
+ bundle add "bridgetown_directus"
38
+ ```
49
39
 
50
- To configure the plugin:
40
+ 2. Run `bundle install` to install the gem.
41
+ 3. Create `config/initializers/bridgetown_directus.rb` (see below for configuration).
51
42
 
52
- 1. You can either use environment variables for the API URL and token:
53
- ```bash
54
- export DIRECTUS_API_URL="https://your-directus-instance.com"
55
- export DIRECTUS_AUTH_TOKEN="your-token"
56
- ```
43
+ ## Configuration
44
+
45
+ ### Minimal Example
57
46
 
58
- 2. Or hard-code the values directly in your initializer:
59
47
  ```ruby
60
- init :bridgetown_directus do
61
- api_url "https://your-directus-instance.com"
62
- token "your_token"
63
-
64
- # Required field mappings
65
- mappings do
66
- title "title"
67
- content "content"
68
- slug "slug"
69
- date "date"
70
- category "category"
71
- excerpt "excerpt"
72
- image "image"
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"
52
+
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])
73
61
  end
74
62
  end
75
63
  ```
76
- ## Usage
77
-
78
- Once the plugin is installed and configured, it will fetch posts from your Directus instance during each build. These posts will be generated as in-memory resources, meaning they are not written to disk but are treated as normal posts by Bridgetown.
79
-
80
- ### Directus Setup
81
-
82
- To use the plugin, ensure that you’ve set up a collection in your Directus instance with the following fields (you can name the collection anything you like):
83
64
 
84
- - **title**: The title of the post (Text field)
85
- - **content**: The content of the post (Rich Text or Text field)
86
- - **slug**: Optional. A unique slug for the post (Text field)
87
- - **date**: Optional.The publish date (Datetime field)
88
- - **status**: Optional. The status of the post (Option field with values like “published”, “draft”, etc.)
89
- - **category**: Optional. The category for the post (Text field)
90
- - **excerpt**: Optional. A short excerpt (Text field)
91
- - **image**: Optional. An image associated with the post (File/Media field)
65
+ For custom collections, create a layout file at `src/_layouts/[singular].erb` (e.g., `staff_member.erb`) to control the page rendering.
92
66
 
93
- Make sure the **status** field uses `"published"` for posts that you want to be visible on your site.
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
94
72
 
95
- #### Image Permissions
73
+ #### Example: Customizing a Field
96
74
 
97
- If your posts contain images, and you want to display them in your Bridgetown site, you'll need to ensure that the **directus_files** collection has the appropriate permissions for public access.
75
+ ```ruby
76
+ c.field :slug, "slug" do |value|
77
+ value || "staff_member-#{SecureRandom.hex(4)}"
78
+ end
79
+ ```
98
80
 
99
- 1. **Public Role Configuration:**
100
- - In Directus, navigate to **Settings** > **Roles & Permissions**.
101
- - Select the **Public** role (or create a custom role if needed).
102
- - Under the **Collections** tab, locate the **directus_files** collection.
103
- - Set the **read** permission to **enabled** so that the images can be accessed publicly.
81
+ ### Translations
104
82
 
105
- 2. **Image Uploads and Management:**
106
- - When users upload images to posts, ensure that the images are associated with the **directus_files** collection.
107
- - By default, Directus will store image URLs, which the plugin can reference directly. Ensure that the **image** field or URL is added to the **body** field (or wherever applicable).
83
+ To enable translations for specific fields, add this inside your collection block:
108
84
 
85
+ ```ruby
86
+ c.enable_translations([:title, :content])
87
+ ```
109
88
 
110
- ### Fetching Posts
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.
111
91
 
112
- Posts are fetched from Directus during each build and treated as Bridgetown resources. These resources are available in your site just like regular posts, and you can access them through your templates or layouts.
92
+ ### File Generation & Cleanup
113
93
 
114
- By default, only posts with a status of "published" are fetched from Directus.
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.
115
96
 
116
- ## TODO List
97
+ ### Advanced Configuration
117
98
 
118
- Here are features that are planned for future versions of the plugin:
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
119
104
 
120
- - [ ] Support for Additional Content Types: Extend the plugin to handle other Directus collections and custom content types.
121
- - [ ] Custom Field Mapping via DSL: Implement a DSL for more advanced field mapping.
122
- - [ ] Asset Handling: Add functionality to download and manage images and other assets.
123
- - [ ] Caching & Incremental Builds: Implement caching to improve build performance when fetching content.
124
- - [ ] Draft Previews: Add support for previewing unpublished (draft) posts.
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!
125
106
 
126
- ## Testing
107
+ ### Migrating from 0.1.x
127
108
 
128
- Testing isn’t fully set up yet, but contributions and improvements are welcome.
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.
129
113
 
130
- ## Contributing
114
+ ---
131
115
 
132
- We welcome contributions to this project! To contribute:
116
+ For more details and advanced usage, see the [plugin README](https://github.com/Munkun-Estudio/bridgetown_directus).
133
117
 
134
- 1. Fork the repository
135
- 2. Create a new branch (git checkout -b feature-branch)
136
- 3. Make your changes
137
- 4. Push to the branch (git push origin feature-branch)
138
- 5. Open a Pull Request
118
+ See [CHANGELOG.md](CHANGELOG.md) for upgrade notes and detailed changes.
data/Rakefile CHANGED
@@ -4,7 +4,7 @@ require "rake/testtask"
4
4
  Rake::TestTask.new(:test) do |t|
5
5
  t.libs << "test"
6
6
  t.libs << "lib"
7
- t.test_files = FileList["test/**/test_*.rb"]
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
8
  t.warning = false
9
9
  end
10
10
 
@@ -2,35 +2,63 @@ say_status :directus, "Installing the bridgetown_directus plugin..."
2
2
 
3
3
  # Prompt the user for Directus API URL and Auth Token
4
4
  api_url = ask("What's your Directus instance URL? (Example: https://your-instance.example.com)")
5
- auth_token = ask("What's your Directus API auth token? (Leave blank to use ENV['DIRECTUS_AUTH_TOKEN'])")
6
- collection = ask("What's the name of the collection? (Example: posts")
5
+ auth_token = ask("What's your Directus API auth token? (Leave blank to use ENV['DIRECTUS_API_TOKEN'])")
6
+ directus_collection = ask("What's the Directus collection name (API endpoint/model)? (Example: posts)")
7
+ bridgetown_collection = ask("What's the Bridgetown collection name (used for folder and resource)? (Example: posts)")
7
8
 
8
9
  # Add the bridgetown_directus gem
9
10
  add_gem "bridgetown_directus"
10
11
 
11
- # Add Directus configuration to config/initializers.rb using add_initializer method
12
- add_initializer :"bridgetown_directus" do
12
+ # Add minimal Directus configuration to config/initializers.rb in the idiomatic Bridgetown plugin style
13
+ add_initializer :bridgetown_directus do |directus|
13
14
  <<~RUBY
14
- do
15
- api_url "#{api_url}"
16
- token "#{auth_token.present? ? auth_token : "<%= ENV['DIRECTUS_AUTH_TOKEN'] %>"}"
17
- collection "#{collection}"
18
-
19
- # Field Mappings (Ensure your Directus collection has these fields)
20
- mappings do
21
- title "title" # Required field
22
- content "content" # Required field
23
- slug "slug" # Optional, will be auto-generated if not provided
24
- date "date" # Optional, defaults to the current date/time if not provided
25
- category "category" # Optional
26
- excerpt "excerpt" # Optional, defaults to content excerpt if not provided
27
- image "image" # Optional, URL for the image associated with the post
28
- end
15
+ # This block was generated by bridgetown_directus automation.
16
+ # All Directus configuration is now handled here (not in YAML).
17
+ # Set DIRECTUS_API_URL and DIRECTUS_API_TOKEN in your .env or shell environment.
18
+ #
19
+ # By default, ALL fields from Directus will be written to the front matter of generated Markdown files.
20
+ # You only need to declare fields here if you want to:
21
+ # - Rename a field in the output
22
+ # - Transform/convert a field value (e.g., format a date, generate a slug, etc.)
23
+ # - Set a default value if a field is missing
24
+ #
25
+ # Example: To customize or transform fields, use the c.field declaration:
26
+ # # require "securerandom" # Uncomment if you use SecureRandom in your mapping
27
+ # c.field :slug, "slug" do |value|
28
+ # value || "post-#{SecureRandom.hex(4)}"
29
+ # end
30
+ #
31
+ # ---
32
+ # TRANSLATIONS:
33
+ # To enable translations for specific fields, add this line inside your collection block:
34
+ # c.enable_translations([:title, :content])
35
+ # You can list any field that exists in your Directus collection, even if it's not declared above with c.field.
36
+ # Declaring a field with c.field is only required if you want to rename, transform, or set a default for it.
37
+ # ---
38
+
39
+ directus.api_url = ENV["DIRECTUS_API_URL"] || "#{api_url}"
40
+ directus.token = ENV["DIRECTUS_API_TOKEN"] || "#{auth_token}"
41
+
42
+ directus.register_collection(:#{bridgetown_collection}) do |c|
43
+ c.endpoint = "#{directus_collection}"
44
+ c.layout = "#{bridgetown_collection.to_s.singularize}"
45
+ # Minimal mapping (optional):
46
+ c.field :id, "id"
47
+ c.field :title, "title"
48
+ # Add more c.field declarations above as needed for custom logic.
49
+ # To enable translations, uncomment and edit the following line:
50
+ # c.enable_translations([:title, :content])
29
51
  end
30
52
  RUBY
31
53
  end
32
- # Success message
33
- say_status :directus, "Directus integration is complete! Please make sure your Directus collection contains the required fields as specified in the initializer."
34
- say_status :directus, "Check config/initializers.rb for your Directus setup and adjust mappings if necessary."
35
- say_status :directus, "For usage help visit:"
36
- say_status :directus, "https://github.com/Munkun-Estudio/bridgetown_directus/blob/main/README.md"
54
+
55
+ say_status :success, "Bridgetown Directus plugin has been installed!", :green
56
+ say_status :directus, "Check config/initializers.rb for your Directus setup."
57
+ say_status :directus, "Check bridgetown.config.yml for your collection setup."
58
+
59
+ # Only remind the user to create a layout if it's a custom collection (not 'posts' or 'pages')
60
+ if !%w[posts pages].include?(bridgetown_collection.to_s)
61
+ say_status :directus, "Don't forget to create a layout file at src/_layouts/#{bridgetown_collection.to_s.singularize}.erb for your custom collection pages!"
62
+ end
63
+
64
+ say_status :directus, "For advanced usage and field customization, see the README: https://github.com/Munkun-Estudio/bridgetown_directus"
@@ -15,9 +15,16 @@ Gem::Specification.new do |spec|
15
15
  spec.test_files = spec.files.grep(%r!^test/!)
16
16
  spec.require_paths = ["lib"]
17
17
 
18
+ spec.metadata = {
19
+ "source_code_uri" => spec.homepage,
20
+ "bug_tracker_uri" => "#{spec.homepage}/issues",
21
+ "changelog_uri" => "#{spec.homepage}/releases",
22
+ "homepage_uri" => spec.homepage
23
+ }
24
+
18
25
  spec.required_ruby_version = ">= 2.7.0"
19
26
 
20
- spec.add_dependency "bridgetown", ">= 1.2.0", "< 2.0"
27
+ spec.add_dependency "bridgetown", ">= 2.0.0.beta4", "< 3.0"
21
28
  spec.add_dependency "faraday", "~> 2.12"
22
29
 
23
30
  spec.add_development_dependency "bundler", "~> 2.0"
@@ -25,7 +32,7 @@ Gem::Specification.new do |spec|
25
32
  spec.add_development_dependency "rubocop-bridgetown", "~> 0.3"
26
33
  spec.add_development_dependency "shoulda", "~> 3.0"
27
34
  spec.add_development_dependency "minitest", "~> 5.0"
28
- spec.add_development_dependency "minitest-profile", "~> 0.5"
35
+ spec.add_development_dependency "minitest-profile", "~> 0.0.2"
29
36
  spec.add_development_dependency "minitest-reporters", "~> 1.0"
30
37
  spec.add_development_dependency "webmock", "~> 3.0"
31
38
  end
@@ -0,0 +1,24 @@
1
+ # Example Bridgetown configuration file for the enhanced Directus plugin
2
+
3
+ # Site settings
4
+ title: My Bridgetown Site with Directus
5
+ email: your-email@example.com
6
+ description: >-
7
+ A Bridgetown site powered by Directus headless CMS
8
+ baseurl: ""
9
+ url: ""
10
+
11
+ # Collections
12
+ collections:
13
+ posts:
14
+ output: true
15
+ permalink: /blog/:slug/
16
+ source: _posts
17
+ future: true
18
+ staff_members:
19
+ output: true
20
+ permalink: /staff_members/:slug/
21
+ source: _staff_members
22
+ sort_field: 'created_at'
23
+ sort_reverse: false
24
+
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Example Bridgetown Directus plugin initializer for v2+
4
+ #
5
+ # All Directus configuration is now handled here, NOT in bridgetown.config.yml.
6
+ # Use ENV variables for secrets and API credentials.
7
+
8
+ require "securerandom"
9
+ require "time"
10
+
11
+ init :bridgetown_directus do |directus|
12
+ # Set API credentials from environment variables
13
+ directus.api_url = ENV["DIRECTUS_API_URL"] || "https://your-directus-instance.com"
14
+ directus.token = ENV["DIRECTUS_API_TOKEN"] || "your-token-here"
15
+
16
+ # Example custom collection: staff_members
17
+ directus.register_collection(:staff_members) do |c|
18
+ c.endpoint = "staff_members"
19
+ c.layout = "staff_member"
20
+ c.field :id, "id"
21
+ c.field :title, "title"
22
+ # To enable translations, uncomment and edit:
23
+ # c.enable_translations([:title, :content])
24
+ # Add more fields as needed
25
+ end
26
+
27
+ # Example for posts (if needed)
28
+ directus.register_collection(:posts) do |c|
29
+ c.endpoint = "articles"
30
+ c.layout = "post"
31
+ c.field :title, "title"
32
+ c.field :content, "body"
33
+ end
34
+ end
@@ -1,62 +1,135 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "yaml"
4
+
3
5
  module BridgetownDirectus
4
6
  class Builder < Bridgetown::Builder
5
7
  def build
8
+ config = site.config.bridgetown_directus
6
9
  return if site.ssr?
7
10
 
8
- Utils.log_directus "Connecting to Directus API..."
9
- posts_data = fetch_posts
10
-
11
- Utils.log_directus "Fetched #{posts_data.size} posts from Directus."
11
+ config.collections.each_value do |collection_config|
12
+ next unless [:posts, :pages, :custom_collection].include?(collection_config.resource_type)
12
13
 
13
- create_documents(posts_data)
14
+ process_collection(
15
+ client: Client.new(
16
+ api_url: config.api_url,
17
+ token: config.token
18
+ ),
19
+ collection_config: collection_config
20
+ )
21
+ end
14
22
  end
15
23
 
16
24
  private
17
25
 
18
- def fetch_posts
19
- api_client = BridgetownDirectus::APIClient.new(site)
20
- api_client.fetch_posts
26
+ # Determine the output directory for the given collection name
27
+ def collection_directory(collection_name)
28
+ case collection_name.to_s
29
+ when "posts"
30
+ File.join(site.source, "_posts")
31
+ when "pages"
32
+ File.join(site.source, "_pages")
33
+ else
34
+ File.join(site.source, "_#{collection_name}")
35
+ end
36
+ end
37
+
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)
21
49
  end
22
50
 
23
- def create_documents(posts_data)
24
- # Ensure posts_data contains a "data" key and it is an array
25
- if posts_data.is_a?(Hash) && posts_data.key?("data") && posts_data["data"].is_a?(Array)
26
- posts_array = posts_data["data"]
27
- elsif posts_data.is_a?(Array)
28
- posts_array = posts_data
29
- else
30
- raise "Unexpected structure of posts_data: #{posts_data.inspect}"
31
- end
32
-
33
- posts_array.each_with_index do |post, index|
34
-
35
- # Fallback to slugify if no slug is provided
36
- slug = post["slug"] || Bridgetown::Utils.slugify(post["title"])
37
- date = post["date"] || Time.now.iso8601
38
-
39
- # Construct the image URL if the image ID is present
40
- image = post["image"]
41
- image = image ? "#{site.config.bridgetown_directus.api_url}/assets/#{image}" : nil
42
-
43
- begin
44
- add_resource :posts, "#{slug}.md" do
45
- layout "post"
46
- title post["title"]
47
- content post["body"] # Make sure content is directly from post["body"]
48
- date date
49
- category post["category"]
50
- excerpt post["excerpt"]
51
- image image
52
- end
53
- rescue => e
54
- Utils.log_directus "Error processing post at index #{index}: #{e.message}"
55
- raise e
51
+ def build_filename(collection_dir, slug)
52
+ File.join(collection_dir, "#{slug}.md")
53
+ end
54
+
55
+ def transform_item_fields(item, api_url, layout)
56
+ item = item.dup
57
+ if item["image"] && api_url && !item["image"].to_s.start_with?("http://", "https://")
58
+ item["image"] = File.join(api_url, "assets", item["image"])
59
+ end
60
+ item["layout"] = layout if layout
61
+ item
62
+ end
63
+
64
+ def generate_front_matter(item)
65
+ yaml = item.to_yaml
66
+ yaml.sub(%r{^---\s*\n}, "") # Remove leading --- if present
67
+ end
68
+
69
+ def write_markdown_file(filename, front_matter, content)
70
+ File.write(filename, "---\n#{front_matter}---\n\n#{content}")
71
+ end
72
+
73
+ # Remove only plugin-generated Markdown files in the target directory before writing new ones
74
+ def clean_collection_directory(collection_dir)
75
+ require "yaml"
76
+ Dir.glob(File.join(collection_dir, "*.md")).each do |file|
77
+ fm = File.read(file)[%r{\A---.*?---}m]
78
+ File.delete(file) if fm && YAML.safe_load(fm)["directus_generated"]
79
+ rescue StandardError => e
80
+ warn "[BridgetownDirectus] Could not check/delete #{file}: #{e.message}"
81
+ end
82
+ end
83
+
84
+ def process_collection(client:, collection_config:)
85
+ endpoint = collection_config.endpoint || collection_config.name.to_s
86
+ begin
87
+ response = client.fetch_collection(endpoint, collection_config.default_query)
88
+ rescue StandardError => e
89
+ warn "Error fetching collection '#{endpoint}': #{e.message}"
90
+ return
91
+ end
92
+ collection_dir = collection_directory(collection_config.name)
93
+ clean_collection_directory(collection_dir)
94
+ api_url = site.config.bridgetown_directus.api_url
95
+ sanitized_response = sanitize_keys(response)
96
+ sanitized_response.each do |item|
97
+ write_directus_file(item, collection_dir, collection_config.layout, api_url)
98
+ end
99
+ end
100
+
101
+ # Recursively sanitize keys to avoid illegal instance variable names (Ruby 3.4+)
102
+ def sanitize_keys(obj)
103
+ case obj
104
+ when Hash
105
+ obj.each_with_object({}) do |(k, v), h|
106
+ safe_key = if %r{^\d}.match?(k.to_s)
107
+ "n_#{k}"
108
+ else
109
+ k
110
+ end
111
+ h[safe_key] = sanitize_keys(v)
56
112
  end
113
+ when Array
114
+ obj.map { |v| sanitize_keys(v) }
115
+ else
116
+ obj
57
117
  end
118
+ end
58
119
 
59
- Utils.log_directus "Finished generating #{posts_array.size} posts."
120
+ # Recursively log all keys to find problematic ones
121
+ def log_all_keys(obj, path = "")
122
+ case obj
123
+ when Hash
124
+ obj.each do |k, v|
125
+ # puts "[BridgetownDirectus DEBUG] Key at #{path}: #{k.inspect}" if %r{^\d}.match?(k.to_s)
126
+ log_all_keys(v, "#{path}/#{k}")
127
+ end
128
+ when Array
129
+ obj.each_with_index do |v, idx|
130
+ log_all_keys(v, "#{path}[#{idx}]")
131
+ end
132
+ end
60
133
  end
61
134
  end
62
135
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "faraday"
5
+
6
+ module BridgetownDirectus
7
+ # Client for interacting with the Directus API
8
+ class Client
9
+ attr_reader :api_url, :token
10
+
11
+ def initialize(api_url:, token:)
12
+ @api_url = api_url
13
+ @token = token
14
+ return unless @token.nil? || @api_url.nil?
15
+
16
+ raise StandardError, "Invalid Directus configuration: missing API token or URL"
17
+ end
18
+
19
+ def fetch_collection(collection, params = {})
20
+ response = connection.get("/items/#{collection}") do |req|
21
+ req.params.merge!(prepare_params(params))
22
+ end
23
+ handle_response(response)
24
+ end
25
+
26
+ def fetch_item(collection, id, params = {})
27
+ response = connection.get("/items/#{collection}/#{id}") do |req|
28
+ req.params.merge!(prepare_params(params))
29
+ end
30
+ handle_response(response)
31
+ end
32
+
33
+ def fetch_items_with_filter(collection, filter, params = {})
34
+ merged_params = params.merge(filter: filter)
35
+ fetch_collection(collection, merged_params)
36
+ end
37
+
38
+ def fetch_related_items(collection, id, relation, params = {})
39
+ response = connection.get("/items/#{collection}/#{id}/#{relation}") do |req|
40
+ req.params.merge!(prepare_params(params))
41
+ end
42
+ handle_response(response)
43
+ end
44
+
45
+ private
46
+
47
+ def connection
48
+ @connection ||= Faraday.new(url: @api_url) do |faraday|
49
+ faraday.headers["Authorization"] = "Bearer #{@token}"
50
+ faraday.headers["Content-Type"] = "application/json"
51
+ faraday.adapter Faraday.default_adapter
52
+ end
53
+ end
54
+
55
+ def prepare_params(params)
56
+ params.transform_keys(&:to_s)
57
+ end
58
+
59
+ def handle_response(response)
60
+ unless response.success?
61
+ Utils.log_directus "Directus API error: #{response.status} - #{response.body}"
62
+ raise "Directus API error: #{response.status}"
63
+ end
64
+ json = JSON.parse(response.body)
65
+ json["data"] || []
66
+ rescue JSON::ParserError => e
67
+ Utils.log_directus "Failed to parse Directus response: #{e.message}"
68
+ raise
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgetownDirectus
4
+ # Configuration module for Bridgetown Directus plugin
5
+ class Configuration
6
+ attr_reader :collections
7
+ attr_accessor :api_url, :token
8
+
9
+ def initialize
10
+ @collections = {}
11
+ end
12
+
13
+ # Register a new collection with the given name
14
+ # @param name [Symbol] The name of the collection
15
+ # @param block [Proc] Configuration block for the collection
16
+ # @return [CollectionConfig] The collection configuration
17
+ def register_collection(name, &block)
18
+ collection = CollectionConfig.new(name)
19
+ collection.instance_eval(&block) if block_given?
20
+ @collections[name] = collection
21
+ collection
22
+ end
23
+
24
+ # Find a collection by name
25
+ # @param name [Symbol] The name of the collection
26
+ # @return [CollectionConfig, nil] The collection configuration or nil if not found
27
+ def find_collection(name)
28
+ @collections[name]
29
+ end
30
+
31
+ # Collection configuration class
32
+ class CollectionConfig
33
+ attr_reader :name
34
+
35
+ # Initialize a new collection configuration
36
+ # @param name [Symbol] The name of the collection
37
+ def initialize(name)
38
+ @name = name
39
+ @fields = {}
40
+ @default_query = {}
41
+ @resource_type = :posts
42
+ @layout = "post"
43
+ @translations_enabled = false
44
+ @translatable_fields = []
45
+ @endpoint = nil
46
+ end
47
+
48
+ # Set up accessors for collection configuration properties
49
+ attr_accessor :endpoint, :fields, :default_query, :resource_type, :layout,
50
+ :translations_enabled, :translatable_fields
51
+
52
+ # Define a field mapping with optional converter
53
+ # @param bridgetown_field [Symbol] The field name in Bridgetown
54
+ # @param directus_field [String, Symbol] The field name in Directus
55
+ # @param converter [Proc, nil] Optional converter to transform the field value
56
+ # @return [void]
57
+ def field(bridgetown_field, directus_field, &converter)
58
+ @fields[bridgetown_field] = {
59
+ directus_field: directus_field.to_s,
60
+ converter: converter,
61
+ }
62
+ end
63
+
64
+ # Enable translations for this collection
65
+ # @param fields [Array<Symbol>] The fields that should be translated
66
+ # @return [void]
67
+ def enable_translations(fields = [])
68
+ @translations_enabled = true
69
+ @translatable_fields = fields
70
+ end
71
+
72
+ # Generate the resource path for a given item
73
+ # @param item [Hash] The data item from Directus
74
+ # @return [String] The resource path
75
+ def path(item)
76
+ # Default: /:resource_type/:slug/index.html
77
+ slug = item["slug"] || item[:slug] || item["id"] || item[:id]
78
+ "/#{resource_type}/#{slug}/index.html"
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgetownDirectus
4
+ # Data mapper for transforming Directus data into Bridgetown resources
5
+ class DataMapper
6
+ class << self
7
+ # Map Directus data to Bridgetown format based on collection configuration
8
+ # @param collection_config [CollectionConfig] The collection configuration
9
+ # @param data [Hash] The Directus data
10
+ # @return [Hash] The mapped data
11
+ def map(collection_config, data)
12
+ mapped_data = {}
13
+
14
+ collection_config.fields.each do |bridgetown_field, field_config|
15
+ if field_config.is_a?(Hash)
16
+ directus_field = field_config[:directus_field]
17
+ converter = field_config[:converter]
18
+
19
+ value = extract_value(data, directus_field)
20
+
21
+ # Apply converter if provided
22
+ value = converter.call(value) if converter.respond_to?(:call)
23
+
24
+ mapped_data[bridgetown_field] = value
25
+ else
26
+ # Support for simple string mapping for backward compatibility
27
+ directus_field = field_config.to_s
28
+ mapped_data[bridgetown_field] = extract_value(data, directus_field)
29
+ end
30
+ end
31
+
32
+ mapped_data
33
+ end
34
+
35
+ # Map translated fields from Directus data
36
+ # @param collection_config [CollectionConfig] The collection configuration
37
+ # @param data [Hash] The Directus data
38
+ # @param locale [Symbol] The locale to map
39
+ # @return [Hash] The mapped data with translations
40
+ def map_translations(collection_config, data, locale)
41
+ # First map the base data
42
+ mapped_data = map(collection_config, data)
43
+
44
+ # If translations are enabled and the data has translations
45
+ return mapped_data unless collection_config.translations_enabled && data["translations"]
46
+
47
+ # Find the translation for the requested locale
48
+ translation = find_translation_for_locale(data["translations"], locale)
49
+
50
+ # Apply translations if found
51
+ if translation
52
+ apply_translations(collection_config, translation, mapped_data)
53
+ mapped_data[:locale] = locale
54
+ end
55
+
56
+ mapped_data
57
+ end
58
+
59
+ # Resolve relationships in the data
60
+ # @param client [Client] The Directus client
61
+ # @param collection_config [CollectionConfig] The collection configuration
62
+ # @param data [Hash] The mapped data
63
+ # @param relationships [Hash] Relationship configuration
64
+ # @return [Hash] The data with resolved relationships
65
+ def resolve_relationships(client, collection_config, data, relationships)
66
+ return data unless relationships
67
+
68
+ resolved_data = data.dup
69
+
70
+ relationships.each do |field, relationship_config|
71
+ relation_id = data[field]
72
+ next unless relation_id
73
+
74
+ related_collection = relationship_config[:collection]
75
+ related_fields = relationship_config[:fields] || "*"
76
+
77
+ # Fetch the related item
78
+ related_item = client.fetch_item(
79
+ related_collection,
80
+ relation_id,
81
+ { fields: related_fields }
82
+ )
83
+
84
+ # Add the related data to the resolved data
85
+ if related_item && related_item["data"]
86
+ resolved_data["#{field}_data"] = related_item["data"]
87
+ end
88
+ end
89
+
90
+ resolved_data
91
+ end
92
+
93
+ private
94
+
95
+ # Find translation for a specific locale
96
+ # @param translations [Array] Array of translation objects
97
+ # @param locale [Symbol] The locale to find
98
+ # @return [Hash, nil] The translation for the locale or nil if not found
99
+ def find_translation_for_locale(translations, locale)
100
+ translations.find do |t|
101
+ lang_code = t["languages_code"].to_s.split("-").first.downcase
102
+ lang_code == locale.to_s
103
+ end
104
+ end
105
+
106
+ # Apply translations to mapped data
107
+ # @param collection_config [CollectionConfig] The collection configuration
108
+ # @param translation [Hash] The translation data
109
+ # @param mapped_data [Hash] The mapped data to update
110
+ # @return [void]
111
+ def apply_translations(collection_config, translation, mapped_data)
112
+ collection_config.translatable_fields.each do |field|
113
+ # Get the Directus field name for this Bridgetown field
114
+ field_config = collection_config.fields[field]
115
+
116
+ directus_field = field_config.is_a?(Hash) ? field_config[:directus_field] : field_config.to_s
117
+
118
+ # Check if the translation has this field
119
+ next unless translation[directus_field]
120
+
121
+ value = translation[directus_field]
122
+
123
+ # Apply converter if provided
124
+ if field_config.is_a?(Hash) && field_config[:converter].respond_to?(:call)
125
+ value = field_config[:converter].call(value)
126
+ end
127
+
128
+ mapped_data[field] = value
129
+ end
130
+ end
131
+
132
+ # Extract a value from nested data using dot notation
133
+ # @param data [Hash] The data to extract from
134
+ # @param field [String] The field path (e.g., "user.profile.name")
135
+ # @return [Object] The extracted value
136
+ def extract_value(data, field)
137
+ return nil unless data
138
+
139
+ keys = field.to_s.split(".")
140
+ value = data
141
+
142
+ keys.each do |key|
143
+ return nil unless value.is_a?(Hash) && value.key?(key)
144
+
145
+ value = value[key]
146
+ end
147
+
148
+ value
149
+ end
150
+ end
151
+ end
152
+ end
@@ -3,7 +3,12 @@
3
3
  module BridgetownDirectus
4
4
  module Utils
5
5
  def self.log_directus(message)
6
- Bridgetown.logger.info("Directus") { message }
6
+ if defined?(Bridgetown) && Bridgetown.respond_to?(:logger)
7
+ Bridgetown.logger.info("Directus") { message }
8
+ elsif ENV["BRIDGETOWN_DIRECTUS_DEBUG"]
9
+ # Fallback for testing or when Bridgetown is not available
10
+ puts "[Directus] #{message}"
11
+ end
7
12
  end
8
13
  end
9
14
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BridgetownDirectus
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -1,27 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bridgetown"
4
- require_relative "bridgetown_directus/version"
5
4
  require_relative "bridgetown_directus/utils"
6
- require_relative "bridgetown_directus/api_client"
5
+ require_relative "bridgetown_directus/client"
6
+ require_relative "bridgetown_directus/data_mapper"
7
+ require_relative "bridgetown_directus/configuration"
7
8
  require_relative "bridgetown_directus/builder"
8
9
 
9
10
  module BridgetownDirectus
10
11
  # Bridgetown initializer for the plugin
11
- Bridgetown.initializer :bridgetown_directus do |config, api_url:, token:, collection:, mappings:|
12
- config.bridgetown_directus ||= {}
13
- config.bridgetown_directus.api_url ||= api_url || ENV.fetch("DIRECTUS_API_URL")
14
- config.bridgetown_directus.token ||= token || ENV.fetch("DIRECTUS_API_TOKEN")
15
- config.bridgetown_directus.collection ||= collection
16
- config.bridgetown_directus.mappings ||= mappings
12
+ Bridgetown.initializer :bridgetown_directus do |config|
13
+ # Only assign config.bridgetown_directus if not already set
14
+ config.bridgetown_directus ||= Configuration.new
15
+
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"
17
19
 
18
20
  # Register the builder
19
21
  config.builder BridgetownDirectus::Builder
22
+ end
23
+
24
+ class Configuration
25
+ attr_accessor :api_url, :token
26
+ attr_reader :collections
27
+
28
+ def initialize
29
+ @collections = {}
30
+ end
20
31
 
21
- # Validate Directus config before proceeding
22
- unless config.bridgetown_directus.api_url && config.bridgetown_directus.token
23
- Bridgetown.logger.error "Invalid Directus configuration detected. Please check your API URL and token."
24
- raise "Directus configuration invalid"
32
+ def register_collection(name, &block)
33
+ collection = CollectionConfig.new(name)
34
+ collection.instance_eval(&block) if block_given?
35
+ @collections[name] = collection
36
+ collection
25
37
  end
26
38
  end
27
39
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bridgetown_directus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Munkun
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-10-15 00:00:00.000000000 Z
10
+ date: 2025-04-16 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bridgetown
@@ -16,20 +15,20 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: 1.2.0
18
+ version: 2.0.0.beta4
20
19
  - - "<"
21
20
  - !ruby/object:Gem::Version
22
- version: '2.0'
21
+ version: '3.0'
23
22
  type: :runtime
24
23
  prerelease: false
25
24
  version_requirements: !ruby/object:Gem::Requirement
26
25
  requirements:
27
26
  - - ">="
28
27
  - !ruby/object:Gem::Version
29
- version: 1.2.0
28
+ version: 2.0.0.beta4
30
29
  - - "<"
31
30
  - !ruby/object:Gem::Version
32
- version: '2.0'
31
+ version: '3.0'
33
32
  - !ruby/object:Gem::Dependency
34
33
  name: faraday
35
34
  requirement: !ruby/object:Gem::Requirement
@@ -120,14 +119,14 @@ dependencies:
120
119
  requirements:
121
120
  - - "~>"
122
121
  - !ruby/object:Gem::Version
123
- version: '0.5'
122
+ version: 0.0.2
124
123
  type: :development
125
124
  prerelease: false
126
125
  version_requirements: !ruby/object:Gem::Requirement
127
126
  requirements:
128
127
  - - "~>"
129
128
  - !ruby/object:Gem::Version
130
- version: '0.5'
129
+ version: 0.0.2
131
130
  - !ruby/object:Gem::Dependency
132
131
  name: minitest-reporters
133
132
  requirement: !ruby/object:Gem::Requirement
@@ -156,7 +155,6 @@ dependencies:
156
155
  - - "~>"
157
156
  - !ruby/object:Gem::Version
158
157
  version: '3.0'
159
- description:
160
158
  email: development@munkun.com
161
159
  executables: []
162
160
  extensions: []
@@ -171,17 +169,24 @@ files:
171
169
  - Rakefile
172
170
  - bridgetown.automation.rb
173
171
  - bridgetown_directus.gemspec
172
+ - example/bridgetown.config.yml
173
+ - example/config/initializers.rb
174
174
  - lib/bridgetown_directus.rb
175
- - lib/bridgetown_directus/api_client.rb
176
175
  - lib/bridgetown_directus/builder.rb
176
+ - lib/bridgetown_directus/client.rb
177
+ - lib/bridgetown_directus/configuration.rb
178
+ - lib/bridgetown_directus/data_mapper.rb
177
179
  - lib/bridgetown_directus/utils.rb
178
180
  - lib/bridgetown_directus/version.rb
179
181
  - package.json
180
182
  homepage: https://github.com/munkun-estudio/bridgetown_directus
181
183
  licenses:
182
184
  - MIT
183
- metadata: {}
184
- post_install_message:
185
+ metadata:
186
+ source_code_uri: https://github.com/munkun-estudio/bridgetown_directus
187
+ bug_tracker_uri: https://github.com/munkun-estudio/bridgetown_directus/issues
188
+ changelog_uri: https://github.com/munkun-estudio/bridgetown_directus/releases
189
+ homepage_uri: https://github.com/munkun-estudio/bridgetown_directus
185
190
  rdoc_options: []
186
191
  require_paths:
187
192
  - lib
@@ -196,8 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
196
201
  - !ruby/object:Gem::Version
197
202
  version: '0'
198
203
  requirements: []
199
- rubygems_version: 3.5.18
200
- signing_key:
204
+ rubygems_version: 3.6.2
201
205
  specification_version: 4
202
206
  summary: Use Directus as headless CMS for Bridgetown
203
207
  test_files: []
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BridgetownDirectus
4
- class APIClient
5
- def initialize(site)
6
- @site = site
7
- @api_url = site.config.bridgetown_directus.api_url
8
- @api_token = site.config.bridgetown_directus.token
9
-
10
- raise StandardError, "Invalid Directus configuration: missing API token or URL" if @api_token.nil? || @api_url.nil?
11
- end
12
-
13
- # Main method to fetch posts
14
- def fetch_posts
15
- Utils.log_directus "Request URL: #{@api_url}/items/#{@site.config.bridgetown_directus.collection}"
16
-
17
- response = connection.get("/items/#{@site.config.bridgetown_directus.collection}") do |req|
18
- req.params['filter'] = { status: { _eq: "published" } }.to_json
19
- end
20
-
21
- if response.success?
22
- JSON.parse(response.body) # Return the parsed posts
23
- elsif response.status == 401
24
- raise RuntimeError, "Unauthorized access to Directus API"
25
- else
26
- raise "Error fetching posts: #{response.status} - #{response.body}"
27
- end
28
- rescue Faraday::TimeoutError
29
- raise Faraday::TimeoutError, "The request to fetch posts timed out"
30
- rescue JSON::ParserError
31
- raise JSON::ParserError, "The response from Directus was not valid JSON"
32
- end
33
-
34
- # Setup Faraday connection with authorization headers
35
- def connection
36
- Faraday.new(url: @api_url) do |faraday|
37
- faraday.options.timeout = 5
38
- faraday.options.open_timeout = 2
39
- faraday.headers['Authorization'] = "Bearer #{@api_token}"
40
- faraday.headers['Content-Type'] = 'application/json'
41
- faraday.adapter Faraday.default_adapter
42
- end
43
- end
44
-
45
- # New method for validating the data structure
46
- private def validate_posts_data(posts_data)
47
- if posts_data.is_a?(Hash) && posts_data.key?("data") && posts_data["data"].is_a?(Array)
48
- posts_data["data"]
49
- elsif posts_data.is_a?(Array)
50
- posts_data
51
- else
52
- raise "Unexpected structure of posts_data: #{posts_data.inspect}"
53
- end
54
- end
55
- end
56
- end