From c20a6feb7f349527a6f59051da4da4f81b0135d2 Mon Sep 17 00:00:00 2001 From: Paulo Santos Date: Tue, 18 Mar 2025 16:57:10 +0000 Subject: [PATCH 1/3] Replace custom Webflow client with webflow-rb gem integration --- Gemfile | 2 + Gemfile.lock | 11 ++- lib/medium_to_webflow.rb | 1 - lib/medium_to_webflow/sync_service.rb | 71 ++++++++++++++++-- lib/medium_to_webflow/version.rb | 2 +- lib/medium_to_webflow/webflow/client.rb | 99 ------------------------- medium_to_webflow.gemspec | 1 + 7 files changed, 77 insertions(+), 110 deletions(-) delete mode 100644 lib/medium_to_webflow/webflow/client.rb diff --git a/Gemfile b/Gemfile index 2682c48..30237b4 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,8 @@ source "https://rubygems.org" # Specify your gem's dependencies in medium_to_webflow.gemspec gemspec +gem "webflow-rb", git: "https://github.com/pjpires/webflow-rb.git", ref: "e04902b" + group :development do gem "debug" gem "rake", "~> 13.0" diff --git a/Gemfile.lock b/Gemfile.lock index e744b6d..a8a0b6d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,19 @@ +GIT + remote: https://github.com/pjpires/webflow-rb.git + revision: e04902b84ea39dda09aeac70704e7817bef0c7cb + ref: e04902b + specs: + webflow-rb (1.1.3) + PATH remote: . specs: - medium_to_webflow (0.1.0) + medium_to_webflow (0.2.0) httparty (~> 0.22.0) nokogiri (~> 1.17) rss (~> 0.3.1) thor (~> 1.3) + webflow-rb (~> 1.1) GEM remote: https://rubygems.org/ @@ -103,6 +111,7 @@ DEPENDENCIES rubocop (~> 1.21) rubocop-rake (~> 0.6.0) rubocop-rspec (~> 3.2.0) + webflow-rb! BUNDLED WITH 2.5.23 diff --git a/lib/medium_to_webflow.rb b/lib/medium_to_webflow.rb index 2fb1b4f..a2bfb42 100644 --- a/lib/medium_to_webflow.rb +++ b/lib/medium_to_webflow.rb @@ -9,7 +9,6 @@ require_relative "medium_to_webflow/medium/client" require_relative "medium_to_webflow/medium/post" -require_relative "medium_to_webflow/webflow/client" require_relative "medium_to_webflow/sync_service" require_relative "medium_to_webflow/cli" diff --git a/lib/medium_to_webflow/sync_service.rb b/lib/medium_to_webflow/sync_service.rb index a146cda..6139634 100644 --- a/lib/medium_to_webflow/sync_service.rb +++ b/lib/medium_to_webflow/sync_service.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "webflow" + module MediumToWebflow class SyncService def self.call(**args) @@ -8,7 +10,7 @@ def self.call(**args) def initialize(medium_username:, webflow_api_token:, webflow_collection_id:, field_mappings:) @medium_username = medium_username - @webflow_api_token = webflow_api_token + @webflow_client = Webflow::Client.new(webflow_api_token) @webflow_collection_id = webflow_collection_id @field_mappings = field_mappings @logger = MediumToWebflow.configuration.logger @@ -37,17 +39,70 @@ def fetch_medium_posts end def sync_to_webflow(posts) - webflow_client = Webflow::Client.new( - api_token: @webflow_api_token, - collection_id: @webflow_collection_id, - field_mappings: @field_mappings - ) - posts.each_with_index do |post, index| @logger.debug "Processing post: #{post.title}" - webflow_client.upsert_post(post) + upsert_post(post) @logger.info "Successfully synced: #{post.title} (#{index + 1}/#{posts.count})" end end + + def upsert_post(medium_post) + fields = build_fields(medium_post) + medium_slug_field = @field_mappings.key("slug") + existing_item = find_item_by_slug(medium_post.send(medium_slug_field)) + + if existing_item + handle_existing_item(existing_item, fields, medium_post) + else + create_item(fields) + end + end + + def find_item_by_slug(slug) + @webflow_client.list_items(@webflow_collection_id, query_params: { slug: slug }).first + rescue ::Webflow::Error => e + @logger.error "Error finding item: #{e.message}" + nil + end + + def handle_existing_item(existing_item, fields, medium_post) + if MediumToWebflow.configuration.force_update + @logger.debug "Forcing update of existing item: #{existing_item[:id]}" + @webflow_client.update_item(@webflow_collection_id, existing_item[:id], fields, is_draft: true) + else + @logger.info "Skipping existing item: #{medium_post.title} (use --force-update to override)" + end + rescue ::Webflow::Error => e + raise Error, "Failed to update item: #{e.message}" + end + + def create_item(fields) + @logger.debug "Creating Webflow item in collection: #{@webflow_collection_id}" + @logger.debug "Fields: #{fields.inspect}" if MediumToWebflow.configuration.verbose + + @webflow_client.create_item(@webflow_collection_id, fields, is_draft: true) + rescue ::Webflow::Error => e + raise Error, "Failed to create item: #{e.message}" + end + + def build_fields(medium_post) + @field_mappings.each_with_object({}) do |(medium_field, webflow_field), fields| + value = medium_post.public_send(medium_field) + next if value.nil? + + fields[webflow_field] = process_field_value(medium_field, value) + end + end + + def process_field_value(field, value) + # Handle the image field by converting it to Webflow's expected format { url: "image_url" } + return { url: value } if field == :image_url + + # Convert DateTime/Time objects to ISO8601 format for Webflow's date fields + return value.iso8601 if value.respond_to?(:iso8601) + + # Return value as-is for all other field types + value + end end end diff --git a/lib/medium_to_webflow/version.rb b/lib/medium_to_webflow/version.rb index 3957be2..909fcf4 100644 --- a/lib/medium_to_webflow/version.rb +++ b/lib/medium_to_webflow/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module MediumToWebflow - VERSION = "0.1.0" + VERSION = "0.2.0" end diff --git a/lib/medium_to_webflow/webflow/client.rb b/lib/medium_to_webflow/webflow/client.rb deleted file mode 100644 index a79d148..0000000 --- a/lib/medium_to_webflow/webflow/client.rb +++ /dev/null @@ -1,99 +0,0 @@ -# frozen_string_literal: true - -module MediumToWebflow - module Webflow - class Client - include HTTParty - base_uri "https://api.webflow.com/v2" - headers "Accept" => "application/json" - headers "Content-Type" => "application/json" - - def initialize(api_token:, collection_id:, field_mappings:) - @api_token = api_token - @collection_id = collection_id - @field_mappings = field_mappings - self.class.headers "Authorization" => "Bearer #{api_token}" - @logger = MediumToWebflow.configuration.logger - end - - def upsert_post(medium_post) - fields = build_fields(medium_post) - medium_slug_field = @field_mappings.key("slug") - existing_item = find_item(slug: medium_post.send(medium_slug_field)) - - handle_existing_or_create_item(existing_item, fields, medium_post) - end - - private - - def handle_existing_or_create_item(existing_item, fields, medium_post) - if existing_item - handle_existing_item(existing_item, fields, medium_post) - else - create_item(fields: fields) - end - end - - def handle_existing_item(existing_item, fields, medium_post) - if MediumToWebflow.configuration.force_update - @logger.debug "Forcing update of existing item: #{existing_item["id"]}" - update_item(item_id: existing_item["id"], fields: fields) - else - @logger.info "Skipping existing item: #{medium_post.title} (use --force-update to override)" - end - end - - def find_item(slug:) - response = self.class.get("/collections/#{@collection_id}/items/live", query: { slug: slug }) - - handle_response(response)["items"]&.first - end - - def create_item(fields:) - @logger.debug "Creating Webflow item in collection: #{@collection_id}" - @logger.debug "Fields: #{fields.inspect}" if MediumToWebflow.configuration.verbose - - response = self.class.post("/collections/#{@collection_id}/items/live", body: { - fieldData: fields - }.to_json) - handle_response(response) - end - - def update_item(item_id:, fields:) - @logger.debug "Updating Webflow item: #{item_id} in collection: #{@collection_id}" - @logger.debug "Fields: #{fields.inspect}" if MediumToWebflow.configuration.verbose - - response = self.class.patch("/collections/#{@collection_id}/items/#{item_id}/live", body: { - fieldData: fields - }.to_json) - handle_response(response) - end - - def build_fields(medium_post) - @field_mappings.each_with_object({}) do |(medium_field, webflow_field), fields| - value = medium_post.public_send(medium_field) - next if value.nil? - - fields[webflow_field] = process_field_value(medium_field, value) - end - end - - def process_field_value(field, value) - # Handle the image field by converting it to Webflow's expected format { url: "image_url" } - return { url: value } if field == :image_url - - # Convert DateTime/Time objects to ISO8601 format for Webflow's date fields - return value.iso8601 if value.respond_to?(:iso8601) - - # Return value as-is for all other field types - value - end - - def handle_response(response) - return response.parsed_response if response.success? - - raise Error, "Webflow API error: #{response.code} - #{response.body}" - end - end - end -end diff --git a/medium_to_webflow.gemspec b/medium_to_webflow.gemspec index bf5e464..daa4333 100644 --- a/medium_to_webflow.gemspec +++ b/medium_to_webflow.gemspec @@ -29,4 +29,5 @@ Gem::Specification.new do |spec| spec.add_dependency "nokogiri", "~> 1.17" spec.add_dependency "rss", "~> 0.3.1" spec.add_dependency "thor", "~> 1.3" + spec.add_dependency "webflow-rb", "~> 1.1" end From d05d76d902b0e750d7a0a06abf8a2da10de25623 Mon Sep 17 00:00:00 2001 From: Paulo Santos Date: Tue, 18 Mar 2025 17:14:53 +0000 Subject: [PATCH 2/3] Refactor Webflow integration with adapter pattern --- lib/medium_to_webflow.rb | 1 + lib/medium_to_webflow/sync_service.rb | 63 ++++++++++-------------- lib/medium_to_webflow/webflow/adapter.rb | 48 ++++++++++++++++++ 3 files changed, 74 insertions(+), 38 deletions(-) create mode 100644 lib/medium_to_webflow/webflow/adapter.rb diff --git a/lib/medium_to_webflow.rb b/lib/medium_to_webflow.rb index a2bfb42..145bd7a 100644 --- a/lib/medium_to_webflow.rb +++ b/lib/medium_to_webflow.rb @@ -9,6 +9,7 @@ require_relative "medium_to_webflow/medium/client" require_relative "medium_to_webflow/medium/post" +require_relative "medium_to_webflow/webflow/adapter" require_relative "medium_to_webflow/sync_service" require_relative "medium_to_webflow/cli" diff --git a/lib/medium_to_webflow/sync_service.rb b/lib/medium_to_webflow/sync_service.rb index 6139634..5c7c6ef 100644 --- a/lib/medium_to_webflow/sync_service.rb +++ b/lib/medium_to_webflow/sync_service.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "webflow" - module MediumToWebflow class SyncService def self.call(**args) @@ -10,8 +8,7 @@ def self.call(**args) def initialize(medium_username:, webflow_api_token:, webflow_collection_id:, field_mappings:) @medium_username = medium_username - @webflow_client = Webflow::Client.new(webflow_api_token) - @webflow_collection_id = webflow_collection_id + @webflow_adapter = Webflow::Adapter.new(webflow_api_token, webflow_collection_id) @field_mappings = field_mappings @logger = MediumToWebflow.configuration.logger end @@ -23,7 +20,7 @@ def call medium_posts = fetch_medium_posts @logger.info "Found #{medium_posts.count} posts to sync" - sync_to_webflow(medium_posts) + sync_medium_posts_to_webflow(medium_posts) @logger.info "Sync completed successfully!" rescue StandardError => e @@ -38,65 +35,55 @@ def fetch_medium_posts Medium::Client.new(username: @medium_username).fetch_posts end - def sync_to_webflow(posts) - posts.each_with_index do |post, index| - @logger.debug "Processing post: #{post.title}" - upsert_post(post) - @logger.info "Successfully synced: #{post.title} (#{index + 1}/#{posts.count})" + def sync_medium_posts_to_webflow(medium_posts) + medium_posts.each_with_index do |medium_post, index| + @logger.debug "Processing post: #{medium_post.title}" + sync_medium_post_to_webflow(medium_post) + @logger.info "Successfully synced: #{medium_post.title} (#{index + 1}/#{medium_posts.count})" end end - def upsert_post(medium_post) - fields = build_fields(medium_post) - medium_slug_field = @field_mappings.key("slug") - existing_item = find_item_by_slug(medium_post.send(medium_slug_field)) + def sync_medium_post_to_webflow(medium_post) + fields = build_webflow_fields(medium_post) + medium_slug_field_name = @field_mappings.key("slug") + existing_item = find_webflow_item_by_slug(medium_post.send(medium_slug_field_name)) if existing_item - handle_existing_item(existing_item, fields, medium_post) + handle_existing_webflow_item(existing_item, fields, medium_post) else - create_item(fields) + create_webflow_item(fields) end end - def find_item_by_slug(slug) - @webflow_client.list_items(@webflow_collection_id, query_params: { slug: slug }).first - rescue ::Webflow::Error => e - @logger.error "Error finding item: #{e.message}" - nil + def find_webflow_item_by_slug(slug) + @webflow_adapter.find_by_slug(slug) end - def handle_existing_item(existing_item, fields, medium_post) + def handle_existing_webflow_item(existing_item, fields, medium_post) if MediumToWebflow.configuration.force_update @logger.debug "Forcing update of existing item: #{existing_item[:id]}" - @webflow_client.update_item(@webflow_collection_id, existing_item[:id], fields, is_draft: true) + @webflow_adapter.update_item(existing_item[:id], fields) else @logger.info "Skipping existing item: #{medium_post.title} (use --force-update to override)" end - rescue ::Webflow::Error => e - raise Error, "Failed to update item: #{e.message}" end - def create_item(fields) - @logger.debug "Creating Webflow item in collection: #{@webflow_collection_id}" - @logger.debug "Fields: #{fields.inspect}" if MediumToWebflow.configuration.verbose - - @webflow_client.create_item(@webflow_collection_id, fields, is_draft: true) - rescue ::Webflow::Error => e - raise Error, "Failed to create item: #{e.message}" + def create_webflow_item(fields) + @webflow_adapter.create_item(fields) end - def build_fields(medium_post) - @field_mappings.each_with_object({}) do |(medium_field, webflow_field), fields| - value = medium_post.public_send(medium_field) + def build_webflow_fields(medium_post) + @field_mappings.each_with_object({}) do |(medium_field_name, webflow_field_name), fields| + value = medium_post.public_send(medium_field_name) next if value.nil? - fields[webflow_field] = process_field_value(medium_field, value) + fields[webflow_field_name] = process_field_value(medium_field_name, value) end end - def process_field_value(field, value) + def process_field_value(medium_field_name, value) # Handle the image field by converting it to Webflow's expected format { url: "image_url" } - return { url: value } if field == :image_url + return { url: value } if medium_field_name == :image_url # Convert DateTime/Time objects to ISO8601 format for Webflow's date fields return value.iso8601 if value.respond_to?(:iso8601) diff --git a/lib/medium_to_webflow/webflow/adapter.rb b/lib/medium_to_webflow/webflow/adapter.rb new file mode 100644 index 0000000..ecd8237 --- /dev/null +++ b/lib/medium_to_webflow/webflow/adapter.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require "webflow" + +module MediumToWebflow + module Webflow + class Adapter + def initialize(api_token, collection_id) + @client = ::Webflow::Client.new(api_token) + @collection_id = collection_id + @logger = MediumToWebflow.configuration.logger + end + + def find_by_slug(slug) + @client.list_items(@collection_id, query_params: { slug: slug }).first + rescue ::Webflow::Error => e + handle_error("list_items", e) + nil + end + + def create_item(fields) + @logger.debug "Creating Webflow item in collection: #{@collection_id}" + @logger.debug "Fields: #{fields.inspect}" if MediumToWebflow.configuration.verbose + + @client.create_item(@collection_id, fields, is_draft: true) + rescue ::Webflow::Error => e + handle_error("create_item", e) + end + + def update_item(item_id, fields) + @logger.debug "Updating Webflow item: #{item_id} in collection: #{@collection_id}" + @logger.debug "Fields: #{fields.inspect}" if MediumToWebflow.configuration.verbose + + @client.update_item(@collection_id, item_id, fields, is_draft: true) + rescue ::Webflow::Error => e + handle_error("update_item", e) + end + + private + + def handle_error(operation, error) + error_message = "Webflow #{operation} operation failed: #{error.message}" + @logger.error error_message + raise MediumToWebflow::Error, error_message + end + end + end +end From e8cd1cd295167f701f16f10e97fb1db71709121c Mon Sep 17 00:00:00 2001 From: Paulo Santos Date: Tue, 18 Mar 2025 17:35:20 +0000 Subject: [PATCH 3/3] Pre-release available on rubygems --- Gemfile.lock | 2 +- lib/medium_to_webflow/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a8a0b6d..dba7e1d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,7 +8,7 @@ GIT PATH remote: . specs: - medium_to_webflow (0.2.0) + medium_to_webflow (0.2.0.beta1) httparty (~> 0.22.0) nokogiri (~> 1.17) rss (~> 0.3.1) diff --git a/lib/medium_to_webflow/version.rb b/lib/medium_to_webflow/version.rb index 909fcf4..5a7d023 100644 --- a/lib/medium_to_webflow/version.rb +++ b/lib/medium_to_webflow/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module MediumToWebflow - VERSION = "0.2.0" + VERSION = "0.2.0.beta1" end