I’ve been playing with Cloudinary for image hosting in a web app (direct upload to them). They have a great set of features, whereby a transformation can be specified as part of the image URL.

Pairing this with Trix/ActionText in Rails 6 was simple enough, but there’s a catch later on for embedded images.

Dependencies

First we’ll get ActionText and the Cloudinary gem up and running.

> rails action_text:install
> bundle add cloudinary

Configuration

Then add you cloudinary credentials to the environment. You can obtain the values from the cloudinary.yml and add them to your production (or development) encrypted credentials file:

> rails credentials:edit --environment=production

Here are the keys you need, with values extracted from the cloudinary.yml above:

  cloudinary:
      cloud_name: "example_app"
      api_key: "444444444444444"
      api_secret: "AAAAAAAA-BBBBBBBBBBBBBBBBBB"
      secure: true

With those values safely stored, we need to load them. In a new initializer:

# config/initializers/cloudinary.rb

Cloudinary.config do |config|
  config.cloud_name = Rails.application.credentials.cloudinary[:cloud_name]
  config.api_key = Rails.application.credentials.cloudinary[:api_key]
  config.api_secret = Rails.application.credentials.cloudinary[:api_secret]
  config.secure = true
  config.cdn_subdomain = true
end

Now specify that ActiveStorage should use Cloudinary, editing config/storage.yml. You can also specify other services and local disk:

# config/storage.yml

cloudinary:
    service: Cloudinary
    
local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

and config/environments/production.rb

# config/environments/production.rb

config.active_storage.service = :cloudinary

Javascript

To use direct uploading (uploaded images never touch your server), you will need to include the ActiveStorage JS in app/javascript/packs/application.js. We’re also going to include the Trix editor JS code and ActionText which supports it:

# app/javascript/packs/application.js

require("@rails/activestorage").start()
require("trix")
require("@rails/actiontext")

Ruby & Erb

So now we’re ready for the editor and display of edited content. In our edit .erb template, we will use the trix editor to manage html content. Let’s assume the age-old Post model has title and content attributes, both of type text.

Add the following to your model:

# app/models/post.rb

has_rich_text :content

Note, if you have been upgrading an older Rails project, ensure you have the following in the head section of your application layout <%= csrf_meta_tags %>. This ensures ActionText can send requests to your app without authentication token errors.

# new.html.erb

<%= form_for Post.new do |f| %>
    <%= f.text :title %>
    <%= f.rich_text_area :content %>
    <%= f.submit %>
<% end %>

The rich_area_text input type gives us the wonderful Trix editor, with drag and drop image support. To display the Post, we have the following:

# show.html.erb

<h1><%= @post.title %></h1>

<%= @post.content %>

Done yet?

Not quite. Remember I mentioned a catch? We need to ensure that the Cloudinary image is displayed properly. At the moment, a local URL is likely specified for any images embedded in content. Open up a file that the ActionText install generated for us and modify to use Cloudinary and replace image_tag calls:

# app/views/active_storage/blobs/_blob.html.erb
--- <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
+++ <% if local_assigns[:in_gallery] %>
+++     <%= cl_image_tag blob.key, width: 800, height: 600, c_limit: true %>
+++ <% else %>
+++     <%= cl_image_tag blob.key, width: 1024, height: 768, c_limit: true %>
+++ <% end %>

Note that I chose to remove the ternary operator conditional and replace it with two separate cl_image_tag calls. This allows clearer differences between in_gallery and other views. The additional parameters to cl_image_tag are passed to the cloudinary API as transforms. FYI, c_limit means that an image will not be resized above it’s original size, so the width and height are effectively max values.

How about now?

Yep, we’re done. Assuming you have no CSRF token issues, you should be good to go. Try dragging an image into the editor. It will be immediately uploaded to cloudinary and referenced when your post is submitted.

One thing I’d still like to resolve is the occasional error, which seems to be timing related; submitting the form before the image is uploaded and acknowledged by Cloudinary results in a missing image in the rendered content. We can probably disable the form submit button/action while the upload in in progress, but that will require implementing event hooks. I’m not sure if ActionText can handle that or not yet.