image

Sascha Depold

Engineering Manager

Blog

Contentful ruby apps

Lately I was playing with the content management service Contentful and its ruby gem contentful.rb. In this article you will learn how to kickstart your … contentful … ruby application!

contentful

Contentful is a cloud-based and API-driven content management system that allows the user to structure his data via so-called content models. Those models are the blueprint for the to-be-stored information. One could think of them as database tables which have specific schemata. Once defined the user can create entries that are instances of the blueprints.

This article will explain how to create those schemata via Contentful's web GUI. Furthermore it will be described how a user can write and publish entries. Finally you will learn to access those entries easily within a ruby application. The result will be a tiny (read-only) blog system.

Update

I wrote another post about contentful ruby apps, which introduces some additional helpers and fixes some caveats with the following approach. You can find it here.

Content models

As described, content models are the blueprints for your actual data. To keep things simple we will create two basic blog entities, which are connected with each other: A model blog_post and a model tag. The relationship between is a many-to-many association, meaning that a blog post can have multiple tags and a tag can contain multiple blog posts.

The content model blog_post

Our upcoming blog post will contain the following fields:

  • A mandatory headline which is represented as symbol.
  • A mandatory content which is represented as text.
  • A mandatory publishing date which is represented as date/time.
  • An optional set of tags with a data type validation for tag.

The content model blog_post

An example blog post representation would look like this:

{
  "sys": {
    /* meta data */
  },
  "fields": {
    "headline": "contentful ruby apps",
    "content": "Lately I was playing with the content management service [Contentful](http://contentful.com/) and its ruby gem [contentful.rb](https://github.com/contentful/contentful.rb). In this article you will learn how to kickstart your … contentful … ruby application!\n\n",
    "publishedAt": "2014-08-19T22:00:00+02:00",
    "tags": [ { "sys": { /* meta data */ } } ]
  }
}

The content model tag

The tag model will be as simple as possible and just contain:

  • a mandatory name which is represented as symbol.

The content model tag

An example tag representation would look like this:

{
  "sys": {
    /* meta data */
  },
  "fields": {
    "name": "ruby"
  }
}

Some words about data types

In Contentful you will find a whole bunch of data types which are coming with different features and pitfalls. The previously stated ones have the following meanings:

  • A symbol is a short string that can be used for filtered searches. Symbols can be used to do strict equality checks. Maximum length is 256.
  • A text can contain huge textual content in which you might want to fuzzy search. A strict equality check cannot be done. Maximum length is 50,000.
  • A date/time comes with a handy calendar widget which allows you to choose a date.
  • A set of entries can reference one or many other elements and can furthermore be validated against certain constraints. One of them is a data type.

Talking ruby ...

The upcoming ruby application will use sinatra and the contentful gem. In order to structure the models and to make additions in contentful an ease, we will define a superclass which handles the whole communication with contentful and provides handy, ActiveRecord-esque helper methods. Furthermore every content model will get its own class representation and inherit from that superclass.

Getting started

Too keep things short I will just assume some knowledge about ruby and sinatra and post a short gist for the application's scaffold:

mkdir contentful_blog
cd contentful_blog
bundle init
echo 'gem "activesupport"' >> Gemfile
echo 'gem "contentful"' >> Gemfile
echo 'gem "i18n"' >> Gemfile
echo 'gem "sinatra"' >> Gemfile
bundle
mkdir models
mkdir views

Accessing the data

ContentModel

The most important class of our application is models/content_model.rb which act as superclass for the Contentful companions. Besides taking care of the actual API calls (via the contentful.rb gem) it will also provide handy methods to do full-text searches or to find entries by certain fields. Furthermore you will be able to use finder methods that are based on the field names of the content model.

So here we are:

require "contentful"

class ContentModel < Contentful::Entry
  class << self
    def entry_mapping
      @entry_mapping ||= superclass.descendants.map do |klass|
        [klass::CONTENT_TYPE_ID, klass]
      end.to_h
    end

    def delivery_client
      @delivery_client ||= Contentful::Client.new(
        access_token:    ENV.fetch("CONTENTFUL_ACCESS_TOKEN"),
        space:           ENV.fetch("CONTENTFUL_SPACE_ID"),
        dynamic_entries: :auto,
        entry_mapping:   entry_mapping
      )
    end

    def content_type
      entry_mapping.invert[self]
    end

    def all(options = {})
      locale  = options.delete(:locale) || I18n.locale
      options = options.reverse_merge(
        "content_type" => content_type,
        "locale"       => locale
      )

      delivery_client.entries options
    end

    def first(options = {})
      all(options.merge("limit" => 1)).first
    end

    def full_text_search(needle)
      first("query" => needle)
    end

    def method_missing(method_name, needle, options={})
      field_name = method_name.to_s.match(/^find_by_(.+)/)

      if field_name
        field_name = "fields.#{field_name[1].camelize(:lower)}"
        first(options.merge(field_name => needle))
      else
        super
      end
    end
  end
end

ContentModel children

As the ContentModel already takes care of the core functionality, each child of that class can focus on their specific domain and might define associations or domain specific helper methods. One thing that needs to be defined for every child class is the constant CONTENT_MODEL_ID, though. Here is how an example looks like:

class ExampleContentModel < ContentModel
  CONTENT_TYPE_ID = "a-nice-id"
end

You can find the ID of the content models within the web GUI of Contentful. Just open the tab Content Model and one of the models afterwards. On the top right you will find a button called Info that will open a drawer with meta information about the content type. The id is what you are looking for. Just copy and paste it into your model and you are good to go.

Meta information of the blog post model

BlogPost

The only logic the blog post has to handle is to encode and decode a slug. That feature will be used for generating corresponding url paths. Furthermore we will specify an association to the tags:

class BlogPost < ContentModel
  CONTENT_TYPE_ID = "id-of-the-blog-post-model"

  def self.from_slug(slug)
    find_by_headline(slug.gsub("-", " "))
  end

  def slug
    fields[:headline].gsub(" ", "-")
  end

  def tags
    fields[:tags]
  end
end

Please notice that the method from_slug calls a custom finder method find_by_headline.

Tag

The tag itself could in theory have a whole bunch of interesting methods, but to keep things simple it will just inherit from the superclass and define the needed content type id:

class Tag < ContentModel
  CONTENT_TYPE_ID = "id-of-the-tag-model"
end

One possible method would be a finder for associated blog posts.

Please take the stage, mister!

The sinatra application is pretty simple, though there is one tiny detail which might be good to know and that is the I18n handling. I hooked active_support/core_ext into my app and set the I18n.default_locale to en-US because that is the default locale of a Contentful space:

require "active_support/core_ext"
require "sinatra"

set :root, File.dirname(__FILE__)
set :server, "webrick"

configure do
  I18n.default_locale = "en-US"

  # load the content_model first and afterwards the other models
  require File.join(settings.root, "models", "content_model.rb")
  Dir[File.join(settings.root, "models", "*.rb")].each { |file| require file }
end

get "/" do
  erb :index, locals: { posts: BlogPost.all }
end

get "/:id" do
  erb :show, locals: { post: BlogPost.from_slug(params[:id]) }
end

Uhm, where is the view?

I will skip the details about the view's implementation and instead direct the interested user to the relevant Github pages:

The post listing page The post listing page

The post detail page The post detail page

Famous last words

The complete application can be found on github. In order to start it, you will need a Contentful account as well as a space with the described content models. Before running the application you need to exchange the content model's ids within blog_post.rb and tag.rb. Once thats done you can run the application via:

bundle install
CONTENTFUL_ACCESS_TOKEN=985174a630cf3203f578e747250bd9a9a9b6250e0a0be61367c2e9338b82d983 CONTENTFUL_SPACE_ID=svq072ikci2q bundle exec ruby app.rb

You should now be able to open http://localhost:4567 :-)

Access token? Space ID?

The space id can be found either in the url of the web GUI (it is the string after spaces/) or in the space settings where it states it next to key. The access token can futhermore be obtained from the API tab. Just open the tab, click on API Keys within the delivery api column and create an API key. Once thats done you should stare at the access token.