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..dba7e1d 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.beta1) 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..145bd7a 100644 --- a/lib/medium_to_webflow.rb +++ b/lib/medium_to_webflow.rb @@ -9,7 +9,7 @@ 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/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 a146cda..5c7c6ef 100644 --- a/lib/medium_to_webflow/sync_service.rb +++ b/lib/medium_to_webflow/sync_service.rb @@ -8,8 +8,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_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 @@ -21,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 @@ -36,18 +35,61 @@ def fetch_medium_posts Medium::Client.new(username: @medium_username).fetch_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 - ) + 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 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_webflow_item(existing_item, fields, medium_post) + else + create_webflow_item(fields) + end + end + + def find_webflow_item_by_slug(slug) + @webflow_adapter.find_by_slug(slug) + end + + 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_adapter.update_item(existing_item[:id], fields) + else + @logger.info "Skipping existing item: #{medium_post.title} (use --force-update to override)" + end + end + + def create_webflow_item(fields) + @webflow_adapter.create_item(fields) + end + + 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? - posts.each_with_index do |post, index| - @logger.debug "Processing post: #{post.title}" - webflow_client.upsert_post(post) - @logger.info "Successfully synced: #{post.title} (#{index + 1}/#{posts.count})" + fields[webflow_field_name] = process_field_value(medium_field_name, value) end end + + 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 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) + + # 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..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.1.0" + VERSION = "0.2.0.beta1" end 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 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