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 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.
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.
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.
Our upcoming blog post will contain the following fields:
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 tag model will be as simple as possible and just contain:
An example tag representation would look like this:
{
"sys": {
/* meta data */
},
"fields": {
"name": "ruby"
}
}
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:
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.
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
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
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.
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
.
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.
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
I will skip the details about the view's implementation and instead direct the interested user to the relevant Github pages:
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 :-)
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.